aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/json/json_file_test.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/tests/json/json_file_test.rs')
-rw-r--r--crates/shirabe/tests/json/json_file_test.rs462
1 files changed, 406 insertions, 56 deletions
diff --git a/crates/shirabe/tests/json/json_file_test.rs b/crates/shirabe/tests/json/json_file_test.rs
index b6fecc6..0224ae6 100644
--- a/crates/shirabe/tests/json/json_file_test.rs
+++ b/crates/shirabe/tests/json/json_file_test.rs
@@ -1,6 +1,9 @@
//! ref: composer/tests/Composer/Test/Json/JsonFileTest.php
-use shirabe::json::JsonFile;
+use indexmap::IndexMap;
+use shirabe::json::{JsonEncodeOptions, JsonFile, JsonValidationException};
+use shirabe_external_packages::seld::json_lint::ParsingException;
+use shirabe_php_shim::PhpMixed;
/// ref: JsonFileTest::expectParseException
fn expect_parse_exception(text: &str, json: &str) {
@@ -9,6 +12,47 @@ fn expect_parse_exception(text: &str, json: &str) {
assert!(message.contains(text), "expected {text:?} in {message:?}");
}
+/// ref: JsonFileTest::assertJsonFormat
+fn assert_json_format(json: &str, data: &PhpMixed, options: Option<JsonEncodeOptions>) {
+ let json = json.replace('\r', "");
+ match options {
+ None => assert_eq!(json, JsonFile::encode(data)),
+ Some(options) => assert_eq!(json, JsonFile::encode_with_options(data, options)),
+ }
+}
+
+/// PHP's JSON_UNESCAPED_UNICODE-only flag set (used by testOnlyUnicode).
+fn unescaped_unicode_only() -> JsonEncodeOptions {
+ JsonEncodeOptions {
+ unescaped_slashes: false,
+ pretty_print: false,
+ unescaped_unicode: true,
+ indent: JsonFile::INDENT_DEFAULT.to_string(),
+ }
+}
+
+fn fixture_path(name: &str) -> std::path::PathBuf {
+ std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/Json/Fixtures")
+ .join(name)
+}
+
+/// ref: TestCase::createTempFile (PHP tempnam())
+fn create_temp_file() -> String {
+ let mut path = std::env::temp_dir();
+ let unique = format!(
+ "shirabe_jsonfiletest_{}_{}",
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ );
+ path.push(unique);
+ std::fs::write(&path, b"").unwrap();
+ path.to_str().unwrap().to_string()
+}
+
#[test]
#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"]
fn test_parse_error_detect_extra_comma() {
@@ -79,153 +123,459 @@ fn test_parse_error_detect_missing_colon() {
expect_parse_exception("Parse error on line 3", json);
}
-// The encode cases assert JsonFile::encode output for specific PHP flag combinations and
-// data shapes (incl. stdClass vs array). Faithful flag mapping and value modeling are not
-// reproduced here.
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_simple_json_string() {
- todo!()
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "name".to_string(),
+ PhpMixed::String("composer/composer".to_string()),
+ );
+ let json = "{\n \"name\": \"composer/composer\"\n}";
+ assert_json_format(json, &PhpMixed::Array(data), None);
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_trailing_backslash() {
- todo!()
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "Metadata\\".to_string(),
+ PhpMixed::String("src/".to_string()),
+ );
+ let json = "{\n \"Metadata\\\\\": \"src/\"\n}";
+ assert_json_format(json, &PhpMixed::Array(data), None);
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_format_empty_array() {
- todo!()
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert("test".to_string(), PhpMixed::List(vec![]));
+ data.insert("test2".to_string(), PhpMixed::Object(IndexMap::new()));
+ let json = "{\n \"test\": [],\n \"test2\": {}\n}";
+ assert_json_format(json, &PhpMixed::Array(data), None);
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_escape() {
- todo!()
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "Metadata\\\"".to_string(),
+ PhpMixed::String("src/".to_string()),
+ );
+ let json = "{\n \"Metadata\\\\\\\"\": \"src/\"\n}";
+ assert_json_format(json, &PhpMixed::Array(data), None);
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_unicode() {
- todo!()
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "Žluťoučký \" kůň".to_string(),
+ PhpMixed::String("úpěl ďábelské ódy za €".to_string()),
+ );
+ let json = "{\n \"Žluťoučký \\\" kůň\": \"úpěl ďábelské ódy za €\"\n}";
+ assert_json_format(json, &PhpMixed::Array(data), None);
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_only_unicode() {
- todo!()
+ let data = PhpMixed::String("\\/ƌ".to_string());
+ assert_json_format("\"\\\\\\/ƌ\"", &data, Some(unescaped_unicode_only()));
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_escaped_slashes() {
- todo!()
+ let data = PhpMixed::String("\\/foo".to_string());
+ assert_json_format("\"\\\\\\/foo\"", &data, Some(JsonEncodeOptions::none()));
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_escaped_backslashes() {
- todo!()
+ let data = PhpMixed::String("a\\b".to_string());
+ assert_json_format("\"a\\\\b\"", &data, Some(JsonEncodeOptions::none()));
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_escaped_unicode() {
- todo!()
+ let data = PhpMixed::String("ƌ".to_string());
+ assert_json_format("\"\\u018c\"", &data, Some(JsonEncodeOptions::none()));
}
#[test]
-#[ignore = "asserts JsonFile::encode output for specific PHP flag/value combinations; not reproduced here"]
+#[ignore]
fn test_double_escaped_unicode() {
- todo!()
+ let data = PhpMixed::List(vec![
+ PhpMixed::String("Zdjęcia".to_string()),
+ PhpMixed::String("hjkjhl\\u0119kkjk".to_string()),
+ ]);
+ let encoded_data = JsonFile::encode(&data);
+
+ let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapper.insert("t".to_string(), PhpMixed::String(encoded_data));
+ let double_encoded_data = JsonFile::encode(&PhpMixed::Array(wrapper));
+
+ let decoded_data = shirabe_php_shim::json_decode(&double_encoded_data, true).unwrap();
+ let t = decoded_data.as_array().unwrap().get("t").unwrap();
+ let double_data = shirabe_php_shim::json_decode(t.as_string().unwrap(), true).unwrap();
+ assert_eq!(data, double_data);
}
-// These read a fixture composer.json and assert read/write indentation behaviour.
#[test]
-#[ignore = "reads a fixture file and asserts indentation behaviour of JsonFile read/write"]
+#[ignore]
fn test_preserve_indentation_after_read() {
- todo!()
+ let src = fixture_path("tabs.json");
+ let dst = fixture_path("tabs2.json");
+ std::fs::copy(&src, &dst).unwrap();
+
+ let mut json_file = JsonFile::new(dst.to_str().unwrap().to_string(), None, None).unwrap();
+ let _data = json_file.read().unwrap();
+ let mut hash: IndexMap<String, PhpMixed> = IndexMap::new();
+ hash.insert("foo".to_string(), PhpMixed::String("baz".to_string()));
+ json_file.write(PhpMixed::Array(hash)).unwrap();
+
+ assert_eq!(
+ "{\n\t\"foo\": \"baz\"\n}\n",
+ std::fs::read_to_string(&dst).unwrap()
+ );
+
+ std::fs::remove_file(&dst).unwrap();
}
#[test]
-#[ignore = "reads a fixture file and asserts indentation behaviour of JsonFile read/write"]
+#[ignore]
fn test_overwrites_indentation_by_default() {
- todo!()
+ let src = fixture_path("tabs.json");
+ let dst = fixture_path("tabs2.json");
+ std::fs::copy(&src, &dst).unwrap();
+
+ let json_file = JsonFile::new(dst.to_str().unwrap().to_string(), None, None).unwrap();
+ let mut hash: IndexMap<String, PhpMixed> = IndexMap::new();
+ hash.insert("foo".to_string(), PhpMixed::String("baz".to_string()));
+ json_file.write(PhpMixed::Array(hash)).unwrap();
+
+ assert_eq!(
+ "{\n \"foo\": \"baz\"\n}\n",
+ std::fs::read_to_string(&dst).unwrap()
+ );
+
+ std::fs::remove_file(&dst).unwrap();
}
-// validateSchema needs the bundled JSON schema and the json-schema validator plus fixtures.
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_schema_validation() {
- todo!()
+ let path = fixture_path("composer.json");
+ let json = JsonFile::new(path.to_str().unwrap().to_string(), None, None).unwrap();
+ json.validate_schema(JsonFile::STRICT_SCHEMA, None).unwrap();
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_schema_validation_error() {
- todo!()
+ let file = create_temp_file();
+ std::fs::write(&file, b"{ \"name\": null }").unwrap();
+ let json = JsonFile::new(file.clone(), None, None).unwrap();
+ let expected_message = format!("\"{}\" does not match the expected JSON schema", file);
+ let expected_error = "name : NULL value found, but a string is required".to_string();
+
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ assert!(e.get_errors().contains(&expected_error));
+
+ let err = json
+ .validate_schema(JsonFile::LAX_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ assert!(e.get_errors().contains(&expected_error));
+
+ std::fs::remove_file(&file).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_schema_validation_lax_additional_properties() {
- todo!()
+ let file = create_temp_file();
+ std::fs::write(
+ &file,
+ b"{ \"name\": \"vendor/package\", \"description\": \"generic description\", \"foo\": \"bar\" }",
+ )
+ .unwrap();
+ let json = JsonFile::new(file.clone(), None, None).unwrap();
+
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(
+ format!("\"{}\" does not match the expected JSON schema", file),
+ e.get_message()
+ );
+ assert_eq!(
+ &vec![
+ "The property foo is not defined and the definition does not allow additional properties"
+ .to_string()
+ ],
+ e.get_errors()
+ );
+
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+ std::fs::remove_file(&file).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_schema_validation_lax_required() {
- todo!()
+ let file = create_temp_file();
+ let json = JsonFile::new(file.clone(), None, None).unwrap();
+
+ let expected_message = format!("\"{}\" does not match the expected JSON schema", file);
+
+ std::fs::write(&file, b"{ }").unwrap();
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ let errors = e.get_errors();
+ assert!(errors.contains(&"name : The property name is required".to_string()));
+ assert!(errors.contains(&"description : The property description is required".to_string()));
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::write(&file, b"{ \"name\": \"vendor/package\" }").unwrap();
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ assert_eq!(
+ &vec!["description : The property description is required".to_string()],
+ e.get_errors()
+ );
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::write(&file, b"{ \"description\": \"generic description\" }").unwrap();
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ assert_eq!(
+ &vec!["name : The property name is required".to_string()],
+ e.get_errors()
+ );
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::write(&file, b"{ \"type\": \"library\" }").unwrap();
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ let errors = e.get_errors();
+ assert!(errors.contains(&"name : The property name is required".to_string()));
+ assert!(errors.contains(&"description : The property description is required".to_string()));
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::write(&file, b"{ \"type\": \"project\" }").unwrap();
+ let err = json
+ .validate_schema(JsonFile::STRICT_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ let errors = e.get_errors();
+ assert!(errors.contains(&"name : The property name is required".to_string()));
+ assert!(errors.contains(&"description : The property description is required".to_string()));
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::write(
+ &file,
+ b"{ \"name\": \"vendor/package\", \"description\": \"generic description\" }",
+ )
+ .unwrap();
+ json.validate_schema(JsonFile::STRICT_SCHEMA, None).unwrap();
+ json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap();
+
+ std::fs::remove_file(&file).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_custom_schema_validation_lax() {
- todo!()
+ let file = create_temp_file();
+ std::fs::write(
+ &file,
+ b"{ \"custom\": \"property\", \"another custom\": \"property\" }",
+ )
+ .unwrap();
+
+ let schema = create_temp_file();
+ std::fs::write(
+ &schema,
+ b"{ \"properties\": { \"custom\": { \"type\": \"string\" }}}",
+ )
+ .unwrap();
+
+ let json = JsonFile::new(file.clone(), None, None).unwrap();
+
+ json.validate_schema(JsonFile::LAX_SCHEMA, Some(&schema))
+ .unwrap();
+
+ std::fs::remove_file(&file).unwrap();
+ std::fs::remove_file(&schema).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_custom_schema_validation_strict() {
- todo!()
+ let file = create_temp_file();
+ std::fs::write(&file, b"{ \"custom\": \"property\" }").unwrap();
+
+ let schema = create_temp_file();
+ std::fs::write(
+ &schema,
+ b"{ \"properties\": { \"custom\": { \"type\": \"string\" }}}",
+ )
+ .unwrap();
+
+ let json = JsonFile::new(file.clone(), None, None).unwrap();
+
+ json.validate_schema(JsonFile::STRICT_SCHEMA, Some(&schema))
+ .unwrap();
+
+ std::fs::remove_file(&file).unwrap();
+ std::fs::remove_file(&schema).unwrap();
}
#[test]
-#[ignore = "needs JsonFile::validateSchema (json-schema validation) and the schema/fixtures"]
+#[ignore]
fn test_auth_schema_validation_with_custom_data_source() {
- todo!()
+ let json = shirabe_php_shim::json_decode("{\"github-oauth\": \"foo\"}", false).unwrap();
+ let expected_message = "\"COMPOSER_AUTH\" does not match the expected JSON schema".to_string();
+ let expected_error = "github-oauth : String value found, but an object is required".to_string();
+
+ let err = JsonFile::validate_json_schema("COMPOSER_AUTH", &json, JsonFile::AUTH_SCHEMA, None)
+ .unwrap_err();
+ let e = err.downcast_ref::<JsonValidationException>().unwrap();
+ assert_eq!(expected_message, e.get_message());
+ assert_eq!(&vec![expected_error], e.get_errors());
+}
+
+/// ref: shared `$data` for the simple merge-conflict cases.
+fn merge_conflict_simple_data() -> PhpMixed {
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "_readme".to_string(),
+ PhpMixed::List(vec![
+ PhpMixed::String(
+ "This file locks the dependencies of your project to a known state".to_string(),
+ ),
+ PhpMixed::String(
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies"
+ .to_string(),
+ ),
+ PhpMixed::String("This file is @generated automatically".to_string()),
+ ]),
+ );
+ data.insert(
+ "content-hash".to_string(),
+ PhpMixed::String(
+ "VCS merge conflict detected. Please run `composer update --lock`.".to_string(),
+ ),
+ );
+ data.insert("packages".to_string(), PhpMixed::List(vec![]));
+ data.insert("packages-dev".to_string(), PhpMixed::List(vec![]));
+ data.insert("aliases".to_string(), PhpMixed::List(vec![]));
+ data.insert(
+ "minimum-stability".to_string(),
+ PhpMixed::String("stable".to_string()),
+ );
+ data.insert("stability-flags".to_string(), PhpMixed::List(vec![]));
+ data.insert("prefer-stable".to_string(), PhpMixed::Bool(false));
+ data.insert("prefer-lowest".to_string(), PhpMixed::Bool(false));
+ data.insert("platform".to_string(), PhpMixed::List(vec![]));
+ data.insert("platform-dev".to_string(), PhpMixed::List(vec![]));
+ data.insert(
+ "plugin-api-version".to_string(),
+ PhpMixed::String("2.3.0".to_string()),
+ );
+ PhpMixed::Array(data)
}
-// The merge-conflict cases build large lock-file structures (with VCS conflict markers) and
-// assert the resulting ParsingException; the fixtures are sizeable and not reproduced here.
#[test]
-#[ignore = "builds a large lock-file structure with VCS merge markers; not reproduced here"]
+#[ignore]
fn test_composer_lock_file_merge_conflict_simple() {
- todo!()
+ let data = merge_conflict_simple_data();
+ let json =
+ std::fs::read_to_string(fixture_path("composer-lock-merge-conflict-simple.txt")).unwrap();
+ assert_eq!(
+ data,
+ JsonFile::parse_json(Some(&json), Some("/path/to/composer.lock")).unwrap()
+ );
}
#[test]
-#[ignore = "builds a large lock-file structure with VCS merge markers; not reproduced here"]
+#[ignore]
fn test_composer_lock_file_merge_conflict_simple_crlf() {
- todo!()
+ let data = merge_conflict_simple_data();
+ let json =
+ std::fs::read_to_string(fixture_path("composer-lock-merge-conflict-simple.txt")).unwrap();
+ assert_eq!(
+ data,
+ JsonFile::parse_json(Some(&json), Some("/path/to/composer.lock")).unwrap()
+ );
}
#[test]
-#[ignore = "builds a large lock-file structure with VCS merge markers; not reproduced here"]
+#[ignore]
fn test_composer_lock_file_merge_conflict_complex() {
- todo!()
+ // complex files have multiple conflict markers and can thus not be simply resolved
+ let data =
+ std::fs::read_to_string(fixture_path("composer-lock-merge-conflict-complex.txt")).unwrap();
+
+ let err = JsonFile::parse_json(Some(&data), Some("/path/to/composer.lock")).unwrap_err();
+ assert!(err.downcast_ref::<ParsingException>().is_some());
}
#[test]
-#[ignore = "builds a large lock-file structure with VCS merge markers; not reproduced here"]
+#[ignore]
fn test_composer_lock_file_merge_conflict_complex_crlf() {
- todo!()
+ // complex files have multiple conflict markers and can thus not be simply resolved
+ let data = std::fs::read_to_string(fixture_path(
+ "composer-lock-merge-conflict-complex-crlf.txt",
+ ))
+ .unwrap();
+
+ let err = JsonFile::parse_json(Some(&data), Some("/path/to/composer.lock")).unwrap_err();
+ assert!(err.downcast_ref::<ParsingException>().is_some());
}
#[test]
-#[ignore = "builds a large lock-file structure with VCS merge markers; not reproduced here"]
+#[ignore]
fn test_composer_lock_file_merge_conflict_extended() {
- todo!()
+ let data =
+ std::fs::read_to_string(fixture_path("composer-lock-merge-conflict-extended.txt")).unwrap();
+
+ let json = JsonFile::parse_json(Some(&data), Some("/path/to/composer.lock")).unwrap();
+ assert_eq!(
+ "VCS merge conflict detected. Please run `composer update --lock`.",
+ json.as_array()
+ .unwrap()
+ .get("content-hash")
+ .unwrap()
+ .as_string()
+ .unwrap()
+ );
}