From 557e3cee7de792cd233989443ff66e7f8f8a20ef Mon Sep 17 00:00:00 2001 From: Revar Desmera Date: Sun, 9 Aug 2026 22:26:59 -0700 Subject: [PATCH 1/2] Resolve escape sequences in string literals Nothing did. The parser hands back a literal's source text with its backslashes intact, deliberately -- that is what lets a StringLiteral reproduce the source it came from, and what pretty-printing round-trips -- but no one cooked it afterwards, so every escape reached scripts raw. "a\nb" was four characters, a backslash and an 'n' among them, rather than three. The same for \t, \r, \" and \\. unescapeStringLiteral() resolves them where the literal is evaluated, which is both evaluation paths: the tree walk in expr_eval and the constant pool in bytecode_compiler. It returns the input untouched when there is no backslash in it, which is nearly every string, so the common case allocates nothing. A backslash escaping the end of a line contributes nothing at all: the string carries on with no break in it. A CRLF goes whole, unlike the reference implementation, which drops only the LF and leaves the CR in the value -- a stray control character in any string continued in a file written on Windows. A lone CR is still an ordinary escaped character. An unknown escape keeps its character and drops the backslash, matching the reference. Also picks up the parser's fix for the same feature, where a backslash before a newline matched no lexer rule and was printed to stdout by flex's default ECHO. Co-Authored-By: Claude Opus 5 (1M context) --- external/openscad_cpp_parser | 2 +- include/openscad_cpp_evaluator/value.hpp | 20 ++++++++ pyproject.toml | 2 +- src/bytecode_compiler.cpp | 5 +- src/expr_eval.cpp | 2 +- src/value.cpp | 34 +++++++++++++ tests/test_value.cpp | 62 ++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 4 deletions(-) diff --git a/external/openscad_cpp_parser b/external/openscad_cpp_parser index 0789702..b621014 160000 --- a/external/openscad_cpp_parser +++ b/external/openscad_cpp_parser @@ -1 +1 @@ -Subproject commit 0789702ee90854c78d703e8ae56271b3f3b15db7 +Subproject commit b6210145687e73b1034dca2687d018d25fa6fdc9 diff --git a/include/openscad_cpp_evaluator/value.hpp b/include/openscad_cpp_evaluator/value.hpp index e899bf2..91191c4 100644 --- a/include/openscad_cpp_evaluator/value.hpp +++ b/include/openscad_cpp_evaluator/value.hpp @@ -181,6 +181,26 @@ Value matmul(const Value& a, const Value& b); // script's output is shown to land on one. std::string formatNumber(double v); +// Resolve the escape sequences in a string literal's source text. +// +// The parser stores a StringLiteral's text exactly as written, backslashes +// and all, so that a literal still reproduces the source it came from and +// pretty-printing round-trips. Cooking is therefore this side's job, and +// until this existed every escape reached scripts intact: "a\nb" was four +// characters, a backslash and an 'n' among them, rather than three. +// +// \\ \" \n \t \r the character they name +// \ nothing at all -- a line continuation, so the string +// carries on with no break in it. A CRLF goes whole: +// leaving the CR behind, as the reference +// implementation does, puts a stray control character +// into the value of any script written on Windows. +// \ that character, with the backslash dropped +// +// A trailing lone backslash is kept as itself. It can only occur in an +// unterminated literal, which is a parse error long before this runs. +std::string unescapeStringLiteral(const std::string& raw); + // echo()/str()/assert-message display format: "undef" | "true"/"false" | // "[start : step : end]" (range) | formatNumber() (number) | "[e1, e2, ...]" // (list, recursive) | "object(k1 = v1, ...)" ("object()" if empty) | diff --git a/pyproject.toml b/pyproject.toml index 12cf7cb..8540b9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "openscad_cpp_evaluator" -version = "0.27.1" +version = "0.28.0" description = "C++ OpenSCAD evaluator with Python bindings" readme = "README.md" requires-python = ">=3.12" diff --git a/src/bytecode_compiler.cpp b/src/bytecode_compiler.cpp index d96f64d..278998d 100644 --- a/src/bytecode_compiler.cpp +++ b/src/bytecode_compiler.cpp @@ -324,7 +324,10 @@ class Compiler { out.push_back({Op::PushBool, static_cast(node).val ? 1 : 0, 0, nullptr}); return; case NodeKind::StringLiteral: - out.push_back({Op::PushConst, internConst(Value{static_cast(node).val}), 0, + out.push_back({Op::PushConst, + internConst(Value{unescapeStringLiteral( + static_cast(node).val)}), + 0, nullptr}); return; case NodeKind::UndefinedLiteral: diff --git a/src/expr_eval.cpp b/src/expr_eval.cpp index 1bf3b0c..1c2e52f 100644 --- a/src/expr_eval.cpp +++ b/src/expr_eval.cpp @@ -396,7 +396,7 @@ Value Evaluator::evalExpr(const oscad::Expression& node, EvalContext& ctx) { case NodeKind::BooleanLiteral: return Value{static_cast(node).val}; case NodeKind::StringLiteral: - return Value{static_cast(node).val}; + return Value{unescapeStringLiteral(static_cast(node).val)}; case NodeKind::UndefinedLiteral: return Value{}; case NodeKind::CommentedExpr: diff --git a/src/value.cpp b/src/value.cpp index aa2f768..d1c0a32 100644 --- a/src/value.cpp +++ b/src/value.cpp @@ -268,6 +268,40 @@ Value matmul(const Value& a, const Value& b) { return makeList(std::move(out)); } +std::string unescapeStringLiteral(const std::string& raw) { + // Most strings carry no escape at all, and this runs on every + // evaluation of a literal on the tree-walking path -- so don't build a + // second copy of the string unless there is something to change. + const size_t first = raw.find('\\'); + if (first == std::string::npos) return raw; + + std::string out; + out.reserve(raw.size()); + out.append(raw, 0, first); + for (size_t i = first; i < raw.size(); ++i) { + if (raw[i] != '\\' || i + 1 >= raw.size()) { + out.push_back(raw[i]); // a trailing lone backslash stands for itself + continue; + } + const char next = raw[i + 1]; + switch (next) { + case 'n': out.push_back('\n'); ++i; break; + case 't': out.push_back('\t'); ++i; break; + case 'r': out.push_back('\r'); ++i; break; + case '\n': ++i; break; // line continuation: contributes nothing + case '\r': + // Only a CRLF pair is a line continuation; a lone CR is an + // ordinary escaped character like any other. + if (i + 2 < raw.size() && raw[i + 2] == '\n') { i += 2; break; } + out.push_back('\r'); + ++i; + break; + default: out.push_back(next); ++i; break; // \\ and \" land here too + } + } + return out; +} + std::string formatNumber(double v) { if (std::isnan(v)) return "nan"; if (std::isinf(v)) return v > 0 ? "inf" : "-inf"; diff --git a/tests/test_value.cpp b/tests/test_value.cpp index 010bb7c..30a78a9 100644 --- a/tests/test_value.cpp +++ b/tests/test_value.cpp @@ -1,5 +1,7 @@ #include "openscad_cpp_evaluator/value.hpp" +#include "test_helpers.hpp" + #include #include #include @@ -434,3 +436,63 @@ TEST(ExpandIterable, ZeroStepRangeIsNaturallyEmptyNotTooMany) { EXPECT_FALSE(warned); EXPECT_EQ(items.size(), 0u); } + +// -- String literal escapes ---------------------------------------------- + +// The parser hands back a literal's source text with its backslashes +// intact, so that a StringLiteral still reproduces the source and +// pretty-printing round-trips. Nothing resolved them, so every escape +// reached scripts raw: "a\nb" was four characters, a backslash and an 'n' +// among them. +TEST(StringEscapes, EachSequenceResolvesToTheCharacterItNames) { + struct Case { const char* raw; const char* want; const char* what; }; + const Case cases[] = { + {R"(a\\b)", "a\\b", "backslash"}, + {R"(a\nb)", "a\nb", "newline"}, + {R"(a\tb)", "a\tb", "tab"}, + {R"(a\rb)", "a\rb", "carriage return"}, + {R"(a\"b)", "a\"b", "quote"}, + {R"(a\qb)", "aqb", "unknown escape keeps the character, drops the backslash"}, + {"plain", "plain", "no escape at all"}, + {"", "", "empty"}, + {R"(\\)", "\\", "nothing but an escape"}, + {"a\\", "a\\", "trailing lone backslash stands for itself"}, + }; + for (const Case& c : cases) + EXPECT_EQ(unescapeStringLiteral(c.raw), c.want) << c.what; +} + +// A backslash escaping the end of a line continues the string with no +// break in it -- the newline contributes nothing. +TEST(StringEscapes, BackslashNewlineContributesNothing) { + EXPECT_EQ(unescapeStringLiteral("a \\\nb"), "a b") << "LF"; + EXPECT_EQ(unescapeStringLiteral("a \\\r\nb"), "a b") << "CRLF goes whole"; + // Deliberately unlike the reference implementation, which drops only + // the LF and leaves the CR in the value -- a stray control character + // in any string continued in a file written on Windows. + EXPECT_EQ(unescapeStringLiteral("a \\\rb"), "a \rb") + << "a lone CR is an ordinary escaped character, not a line ending"; +} + +// Both evaluation paths build the Value, and only one of them was reached +// by a first attempt at this. +TEST(StringEscapes, BothEvaluationPathsCookTheLiteral) { + // Tree-walking path: a literal at top level. + std::vector echoed; + auto sink = [&echoed](const std::string& m) { echoed.push_back(m); }; + oscadeval::test::evaluateSrc("echo(len(\"a\\nb\"));", sink); + ASSERT_EQ(echoed.size(), 1u); + EXPECT_EQ(echoed[0], "ECHO: 3") << "tree-walking path"; + + // Compiled path: inside a function body, which is what gets compiled + // to bytecode. + echoed.clear(); + oscadeval::test::evaluateSrc("function f() = len(\"a\\nb\");\necho(f());", sink); + ASSERT_EQ(echoed.size(), 1u); + EXPECT_EQ(echoed[0], "ECHO: 3") << "compiled path"; + + echoed.clear(); + oscadeval::test::evaluateSrc("function g() = len(\"a \\\nb\");\necho(g());", sink); + ASSERT_EQ(echoed.size(), 1u); + EXPECT_EQ(echoed[0], "ECHO: 3") << "line continuation, compiled path"; +} From 71ea99182b2056fac38ddff963d2dbe2428f136b Mon Sep 17 00:00:00 2001 From: Revar Desmera Date: Sun, 9 Aug 2026 22:40:36 -0700 Subject: [PATCH 2/2] Use forward slashes for paths embedded in test scripts Windows CI turned red once escapes started being resolved: 43 import, surface and DXF/SVG tests build a script by interpolating a temp path into a string literal, and std::filesystem::path::string() hands back backslashes there. `C:\temp\x.stl` was fine while backslashes passed through untouched; now \t is a tab, which is exactly right and exactly what breaks those paths. generic_string() gives forward slashes, which Windows accepts, and this is what a script has to do for real: a backslash in an OpenSCAD string is an escape, so a Windows path in an import() is written with forward slashes or doubled backslashes. The tests were relying on the bug. Only the sites that interpolate into a quoted string literal changed. `use ` and `include ` are lexed in a separate state that does not process escapes at all, so those keep string(), and none of them failed. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_bytecode_compiler.cpp | 4 +-- tests/test_dxf_svg_import.cpp | 58 ++++++++++++++++---------------- tests/test_import_export.cpp | 26 +++++++------- tests/test_surface.cpp | 16 ++++----- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/tests/test_bytecode_compiler.cpp b/tests/test_bytecode_compiler.cpp index 421d0b1..aef7791 100644 --- a/tests/test_bytecode_compiler.cpp +++ b/tests/test_bytecode_compiler.cpp @@ -1044,7 +1044,7 @@ TEST(BytecodeCompiler, ImportCompilesAsTailPositionBody) { std::ofstream out(path); out << R"({"a": 1, "b": 2})"; } - EXPECT_EQ(runCapturingEcho("function f() = import(\"" + path.string() + "\");\necho(f());"), + EXPECT_EQ(runCapturingEcho("function f() = import(\"" + path.generic_string() + "\");\necho(f());"), "ECHO: object(a = 1, b = 2)"); std::filesystem::remove(path); } @@ -1073,7 +1073,7 @@ TEST(BytecodeCompiler, VmOffAndVmOnAgreeOnEchoAssertImportObjectCases) { const std::string script = "function withEcho(x) = echo(\"trace\", x) x + 1;\n" "function withAssert(x) = assert(x >= 0, \"must be non-negative\") x * 2;\n" "function withImport() = import(\"" + - path.string() + + path.generic_string() + "\");\n" "existing = object(m = 1);\n" "function withObject() = object(n = 2, existing, m = 3);\n" diff --git a/tests/test_dxf_svg_import.cpp b/tests/test_dxf_svg_import.cpp index aafca3e..3d01345 100644 --- a/tests/test_dxf_svg_import.cpp +++ b/tests/test_dxf_svg_import.cpp @@ -38,7 +38,7 @@ const char* kMinimalDxfSquare = TEST(DxfImport, ClosedLwpolylineProducesExpectedBoundingBox) { const auto path = tempPath("square.dxf"); writeFile(path, kMinimalDxfSquare); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); // 4x3 square @@ -49,7 +49,7 @@ TEST(DxfImport, ExpressionContextReturnsRegion) { const auto path = tempPath("square_expr.dxf"); writeFile(path, kMinimalDxfSquare); Evaluator ev; - Value v = asExpr("import(\"" + path.string() + "\")", ev); + Value v = asExpr("import(\"" + path.generic_string() + "\")", ev); const auto& contours = std::get(v)->items; ASSERT_EQ(contours.size(), 1u); const auto& pts = std::get(contours[0])->items; @@ -68,7 +68,7 @@ TEST(DxfImport, LayerFilterExcludesOtherLayers) { "0\nENDSEC\n0\nEOF\n"; const auto path = tempPath("layered.dxf"); writeFile(path, dxf); - Evaluated e = evalSrc("import(file=\"" + path.string() + "\", layer=\"keep\");"); + Evaluated e = evalSrc("import(file=\"" + path.generic_string() + "\", layer=\"keep\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 1.0, 1e-6); @@ -90,7 +90,7 @@ TEST(DxfImport, ClosedPolylineVertexEntityProducesExpectedArea) { "0\nENDSEC\n0\nEOF\n"; const auto path = tempPath("polyline.dxf"); writeFile(path, dxf); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); @@ -111,7 +111,7 @@ TEST(DxfImport, OpenPolylineIsIgnored) { const auto path = tempPath("open_polyline.dxf"); writeFile(path, dxf); Evaluator ev; - auto ast = parseSrc("import(\"" + path.string() + "\");"); + auto ast = parseSrc("import(\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -131,7 +131,7 @@ TEST(DxfImport, UnrecognizedEntityIsSkipped) { "0\nENDSEC\n0\nEOF\n"; const auto path = tempPath("unrecognized.dxf"); writeFile(path, dxf); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); @@ -143,7 +143,7 @@ TEST(DxfImport, NoClosedContoursErrors) { const auto path = tempPath("empty.dxf"); writeFile(path, dxf); Evaluator ev; - auto ast = parseSrc("import(\"" + path.string() + "\");"); + auto ast = parseSrc("import(\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -155,7 +155,7 @@ TEST(DxfImport, NoClosedContoursErrors) { TEST(SvgImport, RectProducesExpectedArea) { const auto path = tempPath("rect.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); @@ -167,7 +167,7 @@ TEST(SvgImport, PathWithGroupTransformIsTranslatedAndYFlipped) { writeFile(path, R"svg( )svg"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); manifold::Rect bounds = e.bodies[0].section->Bounds(); @@ -183,7 +183,7 @@ TEST(SvgImport, PathWithGroupTransformIsTranslatedAndYFlipped) { TEST(SvgImport, CircleApproximatesAnalyticArea) { const auto path = tempPath("circle.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 3.14159265 * 25.0, 1.0); // 32-segment approximation std::filesystem::remove(path); @@ -192,7 +192,7 @@ TEST(SvgImport, CircleApproximatesAnalyticArea) { TEST(SvgImport, CubicBezierPathIsWatertight) { const auto path = tempPath("bezier.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_GT(e.bodies[0].section->Area(), 0.0); @@ -203,7 +203,7 @@ TEST(SvgImport, NoShapesErrors) { const auto path = tempPath("noshapes.svg"); writeFile(path, R"()"); Evaluator ev; - auto ast = parseSrc("import(\"" + path.string() + "\");"); + auto ast = parseSrc("import(\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -220,7 +220,7 @@ TEST(SvgImport, EntityReferencesInAttributeValueDecodedWithoutCorruptingParse) { // area would come out wrong (or parsing would throw). const auto path = tempPath("entities.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); @@ -241,7 +241,7 @@ TEST(SvgImport, CommentCdataPrologAndDoctypeAreSkipped) { )"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); @@ -253,7 +253,7 @@ TEST(SvgImport, TransformMatrixIsApplied) { writeFile(path, R"svg( )svg"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 48.0, 1e-6); // 2x scale in both axes -> 4x area manifold::Rect bounds = e.bodies[0].section->Bounds(); @@ -267,7 +267,7 @@ TEST(SvgImport, TransformScaleWithSeparateXyIsApplied) { writeFile(path, R"svg( )svg"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 72.0, 1e-6); // 2x * 3x -> 6x area std::filesystem::remove(path); @@ -278,7 +278,7 @@ TEST(SvgImport, TransformRotateAboutOriginIsApplied) { writeFile(path, R"svg( )svg"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); // rotation preserves area manifold::Rect bounds = e.bodies[0].section->Bounds(); @@ -294,7 +294,7 @@ TEST(SvgImport, TransformRotateAboutOriginIsApplied) { TEST(SvgImport, HorizontalAndVerticalLineCommands) { const auto path = tempPath("hv.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -303,7 +303,7 @@ TEST(SvgImport, HorizontalAndVerticalLineCommands) { TEST(SvgImport, SmoothCubicCommandFollowsCubic) { const auto path = tempPath("smoothcubic.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_GT(e.bodies[0].section->Area(), 0.0); @@ -313,7 +313,7 @@ TEST(SvgImport, SmoothCubicCommandFollowsCubic) { TEST(SvgImport, SmoothQuadraticCommandFollowsQuadratic) { const auto path = tempPath("smoothquad.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_GT(e.bodies[0].section->Area(), 0.0); @@ -323,7 +323,7 @@ TEST(SvgImport, SmoothQuadraticCommandFollowsQuadratic) { TEST(SvgImport, EllipticalArcCommand) { const auto path = tempPath("arc.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_GT(e.bodies[0].section->Area(), 0.0); @@ -333,7 +333,7 @@ TEST(SvgImport, EllipticalArcCommand) { TEST(SvgImport, PolygonPointsAttribute) { const auto path = tempPath("polygon.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -342,7 +342,7 @@ TEST(SvgImport, PolygonPointsAttribute) { TEST(SvgImport, PolylinePointsAttribute) { const auto path = tempPath("polyline.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -351,7 +351,7 @@ TEST(SvgImport, PolylinePointsAttribute) { TEST(SvgImport, StrayCharacterInPointsListIsSkipped) { const auto path = tempPath("straypoints.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -360,7 +360,7 @@ TEST(SvgImport, StrayCharacterInPointsListIsSkipped) { TEST(SvgImport, StrayCharacterInPathDataIsSkipped) { const auto path = tempPath("straypath.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -374,7 +374,7 @@ TEST(SvgImport, TransformNumberWithExponentIsParsed) { writeFile(path, R"svg( )svg"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); manifold::Rect bounds = e.bodies[0].section->Bounds(); EXPECT_NEAR(bounds.min.x, 10.0, 1e-6); @@ -386,7 +386,7 @@ TEST(SvgImport, PathNumberWithExponentIsParsed) { // path `d` attribute rather than a transform argument list. const auto path = tempPath("exponent_path.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 12.0, 1e-6); std::filesystem::remove(path); @@ -400,7 +400,7 @@ TEST(SvgImport, EllipticalArcWithOutOfRangeRadiiIsScaledUp) { // never hit this path. const auto path = tempPath("arc_scaled.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_GT(e.bodies[0].section->Area(), 0.0); @@ -413,7 +413,7 @@ TEST(SvgImport, MultipleSubpathsInOnePathProduceMultipleContours) { // starting a new one, distinct from an explicit `Z`. const auto path = tempPath("multisubpath.svg"); writeFile(path, R"()"); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].section.has_value()); EXPECT_NEAR(e.bodies[0].section->Area(), 24.0, 1e-6); // two disjoint 4x3 squares diff --git a/tests/test_import_export.cpp b/tests/test_import_export.cpp index db9914b..25b06fa 100644 --- a/tests/test_import_export.cpp +++ b/tests/test_import_export.cpp @@ -40,7 +40,7 @@ Value asExpr(const std::string& code, Evaluator& ev) { TEST(ImportModuleContext, StlRoundTripPreservesVolume) { const auto path = tempPath("cube.stl"); writeCubeAs(path, &writeStl); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].body.has_value()); EXPECT_NEAR(e.bodies[0].body->Volume(), 8.0, 1e-6); @@ -50,7 +50,7 @@ TEST(ImportModuleContext, StlRoundTripPreservesVolume) { TEST(ImportModuleContext, ObjRoundTripPreservesVolume) { const auto path = tempPath("cube.obj"); writeCubeAs(path, &writeObj); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].body.has_value()); EXPECT_NEAR(e.bodies[0].body->Volume(), 8.0, 1e-6); @@ -60,7 +60,7 @@ TEST(ImportModuleContext, ObjRoundTripPreservesVolume) { TEST(ImportModuleContext, OffRoundTripPreservesVolume) { const auto path = tempPath("cube.off"); writeCubeAs(path, &writeOff); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].body.has_value()); EXPECT_NEAR(e.bodies[0].body->Volume(), 8.0, 1e-6); @@ -70,7 +70,7 @@ TEST(ImportModuleContext, OffRoundTripPreservesVolume) { TEST(ImportModuleContext, ThreeMfRoundTripPreservesVolume) { const auto path = tempPath("cube.3mf"); writeCubeAs(path, &writeThreeMf); - Evaluated e = evalSrc("import(\"" + path.string() + "\");"); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].body.has_value()); EXPECT_NEAR(e.bodies[0].body->Volume(), 8.0, 1e-6); @@ -100,7 +100,7 @@ TEST(ImportModuleContext, JsonExtensionErrorsAsGeometryStatement) { out << R"({"a": 1})"; } Evaluator ev; - auto ast = parseSrc("import(\"" + path.string() + "\");"); + auto ast = parseSrc("import(\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -118,7 +118,7 @@ TEST(ImportModuleContext, MalformedMeshFileErrors) { out << "this is not a valid STL file at all"; } Evaluator ev; - auto ast = parseSrc("import(\"" + path.string() + "\");"); + auto ast = parseSrc("import(\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -136,7 +136,7 @@ TEST(ImportModuleContext, NonManifoldMeshWarns) { out << "OFF\n3 1 0\n0 0 0\n1 0 0\n0 1 0\n3 0 1 2\n"; } std::string lastWarning; - Evaluated e = evalSrc("import(\"" + path.string() + "\");", [&](const std::string& msg) { lastWarning = msg; }); + Evaluated e = evalSrc("import(\"" + path.generic_string() + "\");", [&](const std::string& msg) { lastWarning = msg; }); EXPECT_NE(lastWarning.find("import: mesh is not a closed solid"), std::string::npos); // The triangle is now handed back for display rather than dropped: a // file that warns once and then shows nothing gives no way to see what @@ -158,7 +158,7 @@ TEST(ImportModuleContext, EmptyMeshHasNoTrianglesErrors) { std::ofstream out(path); out << "OFF\n0 0 0\n"; } - EXPECT_THROW(evalSrc("import(\"" + path.string() + "\");"), EvalError); + EXPECT_THROW(evalSrc("import(\"" + path.generic_string() + "\");"), EvalError); std::filesystem::remove(path); } @@ -168,7 +168,7 @@ TEST(ImportExpressionContext, StlReturnsVnfShape) { const auto path = tempPath("cube_vnf.stl"); writeCubeAs(path, &writeStl); Evaluator ev; - Value v = asExpr("import(\"" + path.string() + "\")", ev); + Value v = asExpr("import(\"" + path.generic_string() + "\")", ev); const auto& outer = std::get(v)->items; ASSERT_EQ(outer.size(), 2u); const auto& verts = std::get(outer[0])->items; @@ -185,7 +185,7 @@ TEST(ImportExpressionContext, JsonReturnsNativeValues) { out << R"({"name": "x", "n": 3, "nested": {"a": 1, "b": 2}, "list": [1, 2, 3]})"; } Evaluator ev; - Value v = asExpr("import(\"" + path.string() + "\")", ev); + Value v = asExpr("import(\"" + path.generic_string() + "\")", ev); const auto& obj = std::get(v)->items; ASSERT_EQ(obj.size(), 4u); EXPECT_EQ(obj[0].first, "name"); @@ -211,7 +211,7 @@ TEST(ImportExpressionContext, JsonNullValueBecomesUndef) { out << R"({"x": null})"; } Evaluator ev; - Value v = asExpr("import(\"" + path.string() + "\")", ev); + Value v = asExpr("import(\"" + path.generic_string() + "\")", ev); const auto& obj = std::get(v)->items; ASSERT_EQ(obj.size(), 1u); EXPECT_TRUE(std::holds_alternative(obj[0].second)); @@ -227,7 +227,7 @@ TEST(ImportExpressionContext, DxfReturnsRegionContours) { "0\nENDSEC\n0\nEOF\n"; } Evaluator ev; - Value v = asExpr("import(\"" + path.string() + "\")", ev); + Value v = asExpr("import(\"" + path.generic_string() + "\")", ev); const auto& contours = std::get(v)->items; ASSERT_EQ(contours.size(), 1u); std::filesystem::remove(path); @@ -250,7 +250,7 @@ TEST(ImportExpressionContext, MalformedMeshFileErrors) { out << "not a valid STL file"; } Evaluator ev; - EXPECT_THROW(asExpr("import(\"" + path.string() + "\")", ev), EvalError); + EXPECT_THROW(asExpr("import(\"" + path.generic_string() + "\")", ev), EvalError); std::filesystem::remove(path); } diff --git a/tests/test_surface.cpp b/tests/test_surface.cpp index f17a909..2c4677e 100644 --- a/tests/test_surface.cpp +++ b/tests/test_surface.cpp @@ -46,7 +46,7 @@ TEST(Surface, DatFileHeightsMatchExactVertexPositions) { // reference on this shape (12 triangles, bbox [0,0,0]-[2,2,5]). const auto path = tempPath("terrain.dat"); writeDat(path, {{0, 0, 0}, {0, 5, 0}, {0, 0, 0}}); - Evaluated e = evalSrc("surface(file=\"" + path.string() + "\", center=false);"); + Evaluated e = evalSrc("surface(file=\"" + path.generic_string() + "\", center=false);"); ASSERT_EQ(e.bodies.size(), 1u); ASSERT_TRUE(e.bodies[0].body.has_value()); manifold::Box bbox = e.bodies[0].body->BoundingBox(); @@ -60,7 +60,7 @@ TEST(Surface, DatFileHeightsMatchExactVertexPositions) { TEST(Surface, CenterTrueOffsetsXyAroundOrigin) { const auto path = tempPath("terrain_center.dat"); writeDat(path, {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}}); - Evaluated e = evalSrc("surface(file=\"" + path.string() + "\", center=true);"); + Evaluated e = evalSrc("surface(file=\"" + path.generic_string() + "\", center=true);"); manifold::Box bbox = e.bodies[0].body->BoundingBox(); EXPECT_NEAR(bbox.min.x, -1.0, 1e-9); // (cols-1)/2 = 1 EXPECT_NEAR(bbox.max.x, 1.0, 1e-9); @@ -70,7 +70,7 @@ TEST(Surface, CenterTrueOffsetsXyAroundOrigin) { TEST(Surface, FlatGridVolumeMatchesFootprintTimesHeight) { const auto path = tempPath("flat.dat"); writeDat(path, {{3, 3, 3}, {3, 3, 3}, {3, 3, 3}}); - Evaluated e = evalSrc("surface(file=\"" + path.string() + "\");"); + Evaluated e = evalSrc("surface(file=\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].body.has_value()); EXPECT_NEAR(e.bodies[0].body->Volume(), 2.0 * 2.0 * 3.0, 1e-6); // (cols-1)*(rows-1)*height std::filesystem::remove(path); @@ -80,7 +80,7 @@ TEST(Surface, EmptyFileErrors) { const auto path = tempPath("empty.dat"); writeDat(path, {}); Evaluator ev; - auto ast = parseSrc("surface(file=\"" + path.string() + "\");"); + auto ast = parseSrc("surface(file=\"" + path.generic_string() + "\");"); auto scope = oscad::buildScopes(ast); EvalContext ctx = EvalContext::makeRoot(scope.get()); EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError); @@ -92,7 +92,7 @@ TEST(Surface, EmptyFileErrors) { TEST(Surface, UniformGrayImageVolumeMatchesLuminanceFormula) { const auto path = tempPath("flat.png"); writeGrayscalePng(path, 3, 3, std::vector(9, 128)); - Evaluated e = evalSrc("surface(file=\"" + path.string() + "\");"); + Evaluated e = evalSrc("surface(file=\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].body.has_value()); // gray=128 (r=g=b, so the weighted luminance formula reduces to the // input value exactly) -> height = 128/255*100. @@ -104,8 +104,8 @@ TEST(Surface, UniformGrayImageVolumeMatchesLuminanceFormula) { TEST(Surface, InvertFlipsTheHeightMapping) { const auto path = tempPath("flat2.png"); writeGrayscalePng(path, 3, 3, std::vector(9, 200)); - Evaluated normal = evalSrc("surface(file=\"" + path.string() + "\", invert=false);"); - Evaluated inverted = evalSrc("surface(file=\"" + path.string() + "\", invert=true);"); + Evaluated normal = evalSrc("surface(file=\"" + path.generic_string() + "\", invert=false);"); + Evaluated inverted = evalSrc("surface(file=\"" + path.generic_string() + "\", invert=true);"); const double heightNormal = 200.0 / 255.0 * 100.0; const double heightInverted = (255.0 - 200.0) / 255.0 * 100.0; EXPECT_NEAR(normal.bodies[0].body->Volume(), 2.0 * 2.0 * heightNormal, heightNormal * 2e-3); @@ -121,7 +121,7 @@ TEST(Surface, ImageRowOrderMatchesBottomRowIsYZero) { const auto path = tempPath("rowcheck.png"); std::vector px = {0, 0, 255, 255}; // row0: black,black ; row1: white,white writeGrayscalePng(path, 2, 2, px); - Evaluated e = evalSrc("surface(file=\"" + path.string() + "\");"); + Evaluated e = evalSrc("surface(file=\"" + path.generic_string() + "\");"); ASSERT_TRUE(e.bodies[0].body.has_value()); // Slice a thin box at low Y (y in [0,0.5]) and confirm it reaches the // white row's full height (100), not the black row's height (0).