forked from weaselab/weaseljson
schemagen: reject enum values that collide after C++ sanitization
When two distinct JSON enum values sanitized to the same C++ identifier, weaseljson_schemagen.py produced an enum class with duplicate constants, generating invalid C++. Detect this at generation time and emit a clear GenError instead. Also add contrib/schemagen/test_schemagen.py with unit tests for the collision case and the normal generation path, and wire both the Python tests and the existing example.schema.json/test_gen.cpp example into ctest via CMakeLists.txt. Closes #4
This commit is contained in:
@@ -163,6 +163,38 @@ target_link_libraries(mytest PRIVATE ${PROJECT_NAME} doctest nanobench simdjson)
|
||||
target_compile_options(mytest PRIVATE ${TEST_FLAGS})
|
||||
doctest_discover_tests(mytest WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# schemagen tests
|
||||
add_test(
|
||||
NAME schemagen_python_tests
|
||||
COMMAND python3 contrib/schemagen/test_schemagen.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
set(SCHEMAGEN_DIR ${CMAKE_SOURCE_DIR}/contrib/schemagen)
|
||||
set(SCHEMAGEN_SCRIPT ${SCHEMAGEN_DIR}/weaseljson_schemagen.py)
|
||||
set(EXAMPLE_SCHEMA ${SCHEMAGEN_DIR}/example.schema.json)
|
||||
set(GEN_H ${CMAKE_BINARY_DIR}/gen.h)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${GEN_H}
|
||||
COMMAND python3 ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA} -o ${GEN_H} --namespace
|
||||
weasel_schema
|
||||
DEPENDS ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA}
|
||||
COMMENT "Generating gen.h from example.schema.json")
|
||||
|
||||
add_custom_target(schemagen_gen_h DEPENDS ${GEN_H})
|
||||
|
||||
add_executable(schemagen_example ${SCHEMAGEN_DIR}/test_gen.cpp)
|
||||
target_include_directories(schemagen_example PRIVATE include
|
||||
${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(schemagen_example PRIVATE ${PROJECT_NAME})
|
||||
target_compile_options(schemagen_example PRIVATE -Wno-switch-enum)
|
||||
add_dependencies(schemagen_example schemagen_gen_h)
|
||||
|
||||
add_test(
|
||||
NAME schemagen_example
|
||||
COMMAND schemagen_example
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
include(CMakePushCheckState)
|
||||
include(CheckCXXCompilerFlag)
|
||||
cmake_push_check_state()
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for weaseljson_schemagen.py."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SCRIPT = os.path.join(os.path.dirname(__file__), "weaseljson_schemagen.py")
|
||||
|
||||
|
||||
class SchemagenEnumTest(unittest.TestCase):
|
||||
def run_schemagen(self, schema, args=None):
|
||||
"""Run schemagen on a schema dict. Returns (returncode, stdout, stderr)."""
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fp:
|
||||
json.dump(schema, fp)
|
||||
schema_path = fp.name
|
||||
try:
|
||||
cmd = [sys.executable, SCRIPT, schema_path]
|
||||
if args:
|
||||
cmd.extend(args)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
finally:
|
||||
os.unlink(schema_path)
|
||||
|
||||
def test_colliding_enum_values_raise_error(self):
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"role": {"enum": ["foo-bar", "foo_bar"]}},
|
||||
}
|
||||
rc, stdout, stderr = self.run_schemagen(schema)
|
||||
self.assertNotEqual(rc, 0)
|
||||
self.assertIn("foo-bar", stderr)
|
||||
self.assertIn("foo_bar", stderr)
|
||||
self.assertIn("C++ identifier", stderr)
|
||||
|
||||
def test_distinct_enum_values_generate(self):
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"role": {"enum": ["admin", "user", "guest"]}},
|
||||
}
|
||||
rc, stdout, stderr = self.run_schemagen(schema)
|
||||
self.assertEqual(rc, 0, msg=stderr)
|
||||
self.assertIn("enum class Role : int { admin, user, guest };", stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -508,6 +508,15 @@ namespace {ns} {{"""
|
||||
return ""
|
||||
out = []
|
||||
for e in self.b.enums.values():
|
||||
seen = {}
|
||||
for v in e.values:
|
||||
ident = sanitize(v)
|
||||
if ident in seen:
|
||||
raise GenError(
|
||||
f"enum {e.name!r} values {seen[ident]!r} and {v!r} "
|
||||
f"both sanitize to C++ identifier {ident!r}"
|
||||
)
|
||||
seen[ident] = v
|
||||
vals = ", ".join(sanitize(v) for v in e.values)
|
||||
out.append(f"enum class {e.name} : int {{ {vals} }};")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
Reference in New Issue
Block a user