blob: 16764a9d8e666b3b7d261e28da7983a952fc9972 [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for verify_json_schemas.py"""
from tempfile import TemporaryDirectory
import json
import unittest
from typing import Any
from verify_json_schemas import (
fail_on_breaking_changes,
SchemaListMismatchError,
MissingInputError,
GoldenMismatchError,
)
class VerifyJsonSchemaTests(unittest.TestCase):
def generate_path(self, base_dir: str, file_name: str) -> str:
return base_dir + "/" + file_name
def test_fail_on_breaking_changes(self) -> None:
data_A = "some_data_A"
data_B = "some_data_B"
manifest: dict[str, Any] = {"atoms": []}
with TemporaryDirectory() as base_dir:
golden_m = self.generate_path(base_dir, "core.golden")
manifest["atoms"].append(data_A)
with open(golden_m, "w") as f:
json.dump(manifest, f)
current_m = self.generate_path(base_dir, "core")
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a normal case will pass.
self.assertTrue(
fail_on_breaking_changes([current_m], [golden_m]) is None # type: ignore[func-returns-value]
)
manifest["atoms"] = []
manifest["atoms"].append(data_B)
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that schemas with same keys, different values is a non-breaking change.
self.assertRaises(
GoldenMismatchError,
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2: dict[str, Any] = {"required": ["req1", "req2"]}
with open(golden_m, "w") as f:
json.dump(manifest2, f)
manifest2["required"].append("req3")
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Assert that any changes to the values of "required" key are a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*'required' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2["required"].remove("req2")
manifest2["required"].remove("req3")
with open(current_m, "w") as f:
json.dump(manifest2, f)
self.assertRaisesRegex(
GoldenMismatchError,
".*'required' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2["required"] = "data_string"
with open(current_m, "w") as f:
json.dump(manifest2, f)
self.assertRaisesRegex(
GoldenMismatchError,
".*'required' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2 = {"enum": ["req1"]}
with open(golden_m, "w") as f:
json.dump(manifest2, f)
manifest2["enum"] = ["req2"]
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Assert that any changes to the value of an "enum" key is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*'enum' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2["enum"].remove("req2")
with open(current_m, "w") as f:
json.dump(manifest2, f)
self.assertRaisesRegex(
GoldenMismatchError,
".*'enum' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2["enum"] = "data_string"
with open(current_m, "w") as f:
json.dump(manifest2, f)
self.assertRaisesRegex(
GoldenMismatchError,
".*'enum' parameters changed on root.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2 = {"additionalProperties": True}
with open(golden_m, "w") as f:
json.dump(manifest2, f)
manifest2["additionalProperties"] = False
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Changing the value of "additionalProperties" to False is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Value for 'additionalProperties' on root*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
with open(golden_m, "w") as f:
json.dump(manifest2, f)
manifest2 = {"additionalProperties": True}
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Changing the value of "additionalProperties" to True is a non-breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*New value for 'additionalProperties' on root*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest["atoms"].remove(data_B)
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest["next"] = {"some_key": "some value B"}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key addition at root is a non-breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*New keys of root\:\n\s*\{'next'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest["atoms"] = {"some_key": "some value B"}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key addition beneath root is a non-breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*New keys of root.atoms\:\n\s*\{'some_key.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest2 = {
"renamed_atoms": {
"some_key": "some value B",
},
"next": {
"some_key": "some value B",
},
}
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Assert that a schema with a key renamed at root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root\:\n\s*\{'atoms'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest["atoms"] = {"re_key": "some value B"}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key renamed beneath root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.atoms\:\n\s*\{'some_key'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest2 = {
"next": {
"some_key": "some value B",
}
}
with open(current_m, "w") as f:
json.dump(manifest2, f)
# Assert that a schema with a key deleted at root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root\:\n\s*\{'atoms'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest["atoms"] = []
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key deleted beneath root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.atoms\:\n\s*\{'some_key'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"atoms": {"some_key1": "some value B"},
"next": {"some_key2": "some value B"},
}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {
"atoms": {"some_key2": "some value B"},
"next": {"some_key1": "some value B"},
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key relocated at root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.next\:\n\s*\{'some_key2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.atoms\:\n\s*\{'some_key1'\}.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"atoms": {"some_key": {"key1": 1}},
"next": {"some_key": {"key2": 2}},
}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {
"atoms": {"some_key": {"key2": 1}},
"next": {"some_key": {"key1": 2}},
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that a schema with a key relocated beneath root is a breaking change.
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.atoms.some_key\:\n\s*\{'key1'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
self.assertRaisesRegex(
GoldenMismatchError,
".*Missing keys of root.next.some_key\:\n\s*\{'key2'\}.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
mock_gold = "path/to/core.golden"
ret_str = (
f"Detected missing JSON Schema {mock_gold}.\n"
f"Please consult with sdk-dev@fuchsia.dev if you are"
f" planning to remove a schema.\n"
f"If you have approval to make this change, remove the "
f"schema and corresponding golden file from the schema lists.\n"
)
# Assert that a nonexistent golden file will not pass.
self.assertRaises(
MissingInputError,
fail_on_breaking_changes,
[current_m],
[mock_gold],
)
self.assertTrue(
str(MissingInputError(missing=mock_gold)) == ret_str
)
mock_curr = "path/to/core"
ret_str = (
f"Detected missing JSON Schema {mock_curr}.\n"
f"Please consult with sdk-dev@fuchsia.dev if you are"
f" planning to remove a schema.\n"
f"If you have approval to make this change, remove the "
f"schema and corresponding golden file from the schema lists.\n"
)
# Assert that a nonexistent current file will not pass.
self.assertRaises(
MissingInputError,
fail_on_breaking_changes,
[mock_curr],
[golden_m],
)
self.assertTrue(
str(MissingInputError(missing=mock_curr)) == ret_str
)
# Assert that lists of a different size will not pass.
ret_str = (
f"Detected that the golden list contains "
f"a different number of schemas than the current list.\n"
f"Golden:\n['{golden_m}']\nCurrent:\n['{mock_curr}', '{golden_m}']\n"
f"Please make sure each schema has a corresponding golden file,"
f" and vice versa.\n"
)
self.assertRaises(
SchemaListMismatchError,
fail_on_breaking_changes,
[mock_curr, current_m],
[golden_m],
)
self.assertTrue(
str(
SchemaListMismatchError(
err_type=0,
goldens=[golden_m],
currents=[mock_curr, golden_m],
)
)
== ret_str
)
self.assertRaises(
SchemaListMismatchError,
fail_on_breaking_changes,
[current_m],
[mock_gold, golden_m],
)
ret_str = (
f"Detected that the golden list contains "
f"a different number of schemas than the current list.\n"
f"Golden:\n[]\nCurrent:\n['{current_m}']\n"
f"Please make sure each schema has a corresponding golden file,"
f" and vice versa.\n"
)
self.assertRaises(
SchemaListMismatchError,
fail_on_breaking_changes,
[current_m],
[],
)
self.assertTrue(
str(
SchemaListMismatchError(
err_type=0, goldens=[], currents=[current_m]
)
)
== ret_str
)
ret_str = (
f"Detected that the golden list contains "
f"a different number of schemas than the current list.\n"
f"Golden:\n['{golden_m}']\nCurrent:\n[]\n"
f"Please make sure each schema has a corresponding golden file,"
f" and vice versa.\n"
)
self.assertRaises(
SchemaListMismatchError,
fail_on_breaking_changes,
[],
[golden_m],
)
self.assertTrue(
str(
SchemaListMismatchError(
err_type=0, goldens=[golden_m], currents=[]
)
)
== ret_str
)
ret_str = (
f"Detected that filenames in the golden list, ['{current_m}'],"
f" do not correspond to those in the current list, "
f"['{current_m}'].\n"
f"Please make sure each schema has a corresponding golden file,"
f" and vice versa.\n"
)
# Assert that files that would normally pass will fail
# if the golden files do not have the .golden extension.
self.assertRaises(
SchemaListMismatchError,
fail_on_breaking_changes,
[current_m],
[current_m],
)
self.assertTrue(
str(
SchemaListMismatchError(
err_type=1, goldens=[current_m], currents=[current_m]
)
)
== ret_str
)
manifest = {
"properties": {
"prop1": "data",
},
"required": ["prop1"],
}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {
"properties": {"prop1": "data", "prop2": "more_data"},
"required": ["prop1"],
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that adding an optional parameter is not breaking.
self.assertRaisesRegex(
GoldenMismatchError,
"Non-breaking Changes((\n?)|(.*?))*?Changed parameters on level root.properties\:\n\s*\{'prop2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"properties": {"prop1": "data", "prop2": "more_data"},
"required": ["prop1", "prop2"],
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that adding a required parameter is breaking.
self.assertRaisesRegex(
GoldenMismatchError,
"Breaking Changes((\n?)|(.*?))*?Changed parameters on level root.properties\:\n\s*\{'prop2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"properties": {
"prop2": "data",
},
"required": [],
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that removing a 'required' parameter is breaking.
self.assertRaisesRegex(
GoldenMismatchError,
"Breaking Changes((\n?)|(.*?))*?Changed parameters on level root.properties\:\n\s*\{'prop2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"properties": {"prop1": "data", "prop2": "more_data"},
"required": ["prop1"],
"additionalProperties": True,
}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {
"properties": {
"prop1": "data",
},
"required": ["prop1"],
"additionalProperties": True,
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that removing an optional parameter is not breaking
# if 'additionalProperties' is True.
self.assertRaisesRegex(
GoldenMismatchError,
"Non-breaking Changes((\n?)|(.*?))*?Changed parameters on level root.properties\:\n\s*\{'prop2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {
"properties": {"prop1": "data", "prop2": "more_data"},
"required": ["prop1"],
"additionalProperties": False,
}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {
"properties": {
"prop1": "data",
},
"required": ["prop1"],
"additionalProperties": False,
}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that removing an optional parameter is breaking
# if 'additionalProperties' is False.
self.assertRaisesRegex(
GoldenMismatchError,
"Breaking Changes((\n?)|(.*?))*?Changed parameters on level root.properties\:\n\s*\{'prop2'.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {"type": "type_data"}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {"type": "other_type_data"}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that changes to 'type' are breaking
# if the golden value is a string.
self.assertRaisesRegex(
GoldenMismatchError,
"Breaking Changes((\n?)|(.*?))*?Value for 'type' on root should be\:\n\s*type_data.*",
fail_on_breaking_changes,
[current_m],
[golden_m],
)
manifest = {"type": ["type_data"]}
with open(golden_m, "w") as f:
json.dump(manifest, f)
manifest = {"type": ["other_type_data"]}
with open(current_m, "w") as f:
json.dump(manifest, f)
# Assert that changes to 'type' is not breaking
# if the golden value is not a string.
self.assertRaises(
GoldenMismatchError,
fail_on_breaking_changes,
[current_m],
[golden_m],
)
if __name__ == "__main__":
unittest.main()