From 0b0e027f880df88ea5edf3d4bf315b7494e00e82 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:16:30 +0200 Subject: [PATCH 1/8] Add a failing spec for transforming a file with no beams A jbeam file is not obliged to have a beams section, and classifying, sorting and renaming need none. Only support classification reads beams, and without them the answer is that there are no support nodes. transform fails the whole file instead, and the tool still exits 0, so a run over a directory leaves such files untouched without saying why. --- .../regression_jbeam/no-beams-repro.jbeam | 18 +++++++++++++++ test-extra/transformation/Spec.hs | 1 + test-extra/transformation/Spec/Regression.hs | 22 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 examples/regression_jbeam/no-beams-repro.jbeam diff --git a/examples/regression_jbeam/no-beams-repro.jbeam b/examples/regression_jbeam/no-beams-repro.jbeam new file mode 100644 index 00000000..2c38b8a5 --- /dev/null +++ b/examples/regression_jbeam/no-beams-repro.jbeam @@ -0,0 +1,18 @@ +{ +"testpart":{ + "nodes":[ + ["id", "posX", "posY", "posZ"], + // Synthetic regression-test fixture, not vetted by the jbeam + // maintainer and not intended as a demo/example. + // + // Nodes and nothing else. A jbeam file is not obliged to have a beams + // section, and classifying, sorting and renaming need no beams at all. + // Only support classification does, and its answer here is simply that + // there are none. + ["nl0", 0.9, -1.0, 0.1], + ["nl1", 0.9, 0.0, 0.1], + ["nr2", -0.9, -1.0, 0.1], + ["nr3", -0.9, 0.0, 0.1], + ], +}, +} diff --git a/test-extra/transformation/Spec.hs b/test-extra/transformation/Spec.hs index 201a0b7c..6b828d84 100644 --- a/test-extra/transformation/Spec.hs +++ b/test-extra/transformation/Spec.hs @@ -160,6 +160,7 @@ main = hspec $ do letterEndingNodesSpec ySortingBandingSpec xColumnSortingSpec + noBeamsSpec metadataAcrossTreesSpec metadataPreservedSpec triangleMetadataSpec diff --git a/test-extra/transformation/Spec/Regression.hs b/test-extra/transformation/Spec/Regression.hs index 71aaf418..c98f28f6 100644 --- a/test-extra/transformation/Spec/Regression.hs +++ b/test-extra/transformation/Spec/Regression.hs @@ -10,6 +10,7 @@ module Spec.Regression ( metadataAcrossTreesSpec, metadataPreservedSpec, xColumnSortingSpec, + noBeamsSpec, ) where import Data.Map qualified as M @@ -220,3 +221,24 @@ xColumnSortingSpec = case transform M.empty columnSortingConfig node of Left err -> expectationFailure ("transform failed: " ++ T.unpack err) Right (_, _, _, resultNode) -> assert resultNode + +{- | A jbeam file is not obliged to have a beams section. Classifying, sorting +and renaming need none: only support classification reads beams, and its +answer without them is that there are no support nodes. Issue #229. + +`transform` instead fails the whole file, and the tool still exits 0, so a +run over a directory leaves such files untouched without saying why. +-} +noBeamsFixture :: FilePath +noBeamsFixture = "examples/regression_jbeam/no-beams-repro.jbeam" + +noBeamsSpec :: Spec +noBeamsSpec = + describe "a file with no beams section" + . it "is transformed, with no node classified as support" + $ do + topNode <- parseJbeamFile noBeamsFixture + case transform M.empty newTransformationConfig topNode of + Left err -> expectationFailure ("transform failed: " ++ T.unpack err) + Right (_, _, _, resultNode) -> + length (vertexCoordinates resultNode) `shouldBe` 4 From ec0b0d7163f973d99e7be7fd4d33124477aea6c2 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:37:48 +0200 Subject: [PATCH 2/8] Check that no beams means no support node The spec said it classified nothing as support and only counted the nodes, which stays at four whether or not a support tree is created, since support extraction moves nodes rather than dropping them. It now looks for the side comment the transformation writes above a support tree, which is absent exactly when no such tree exists. --- test-extra/transformation/Spec/Helpers.hs | 22 +++++++++++++++++++- test-extra/transformation/Spec/Regression.hs | 16 +++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/test-extra/transformation/Spec/Helpers.hs b/test-extra/transformation/Spec/Helpers.hs index 2b8b944f..cfd1be59 100644 --- a/test-extra/transformation/Spec/Helpers.hs +++ b/test-extra/transformation/Spec/Helpers.hs @@ -7,6 +7,7 @@ module Spec.Helpers ( effectiveMetaByCoordinate, metaNumber, outOfOrderPairs, + commentTexts, ) where import Data.Char (isDigit) @@ -17,7 +18,14 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Vector qualified as V import GHC.IsList (fromList) -import JbeamEdit.Core.Node (Node (..), NumberValue (..), expectArray) +import JbeamEdit.Core.Node ( + InternalComment (..), + Node (..), + NumberValue (..), + avNodes, + expectArray, + ovNodes, + ) import JbeamEdit.Core.NodePath qualified as NP import JbeamEdit.IOUtils (tryReadFile) import JbeamEdit.Parsing.Jbeam (parseNodes) @@ -153,3 +161,15 @@ outOfOrderPairs thr resultNode = where positions = zip (vertexPositionsInOrder resultNode) [0 :: Int ..] groupPrefix = T.dropWhileEnd isDigit + +{- | Every comment in a top node, in no particular order. The transformation +writes a side comment above each tree it creates, so the absence of one names +a tree that was never created. +-} +commentTexts :: Node -> [Text] +commentTexts node = case node of + Comment comment -> [cText comment] + Array arrayValue -> concatMap commentTexts (V.toList (avNodes arrayValue)) + Object objectValue -> concatMap commentTexts (V.toList (ovNodes objectValue)) + ObjectKey (key, value) -> commentTexts key ++ commentTexts value + _ -> [] diff --git a/test-extra/transformation/Spec/Regression.hs b/test-extra/transformation/Spec/Regression.hs index c98f28f6..e9b51ad4 100644 --- a/test-extra/transformation/Spec/Regression.hs +++ b/test-extra/transformation/Spec/Regression.hs @@ -234,11 +234,17 @@ noBeamsFixture = "examples/regression_jbeam/no-beams-repro.jbeam" noBeamsSpec :: Spec noBeamsSpec = - describe "a file with no beams section" - . it "is transformed, with no node classified as support" - $ do + describe "a file with no beams section" $ do + it "is transformed, keeping every node" $ + withNoBeams $ \resultNode -> + length (vertexCoordinates resultNode) `shouldBe` 4 + + it "classifies no node as support" $ + withNoBeams $ \resultNode -> + commentTexts resultNode `shouldNotContain` ["Support nodes"] + where + withNoBeams assert = do topNode <- parseJbeamFile noBeamsFixture case transform M.empty newTransformationConfig topNode of Left err -> expectationFailure ("transform failed: " ++ T.unpack err) - Right (_, _, _, resultNode) -> - length (vertexCoordinates resultNode) `shouldBe` 4 + Right (_, _, _, resultNode) -> assert resultNode From 2a42c1fb20a1e8d6c7a0767abe59522a61c89aa2 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:38:21 +0200 Subject: [PATCH 3/8] Drop the redundant dollars hlint flagged --- test-extra/transformation/Spec/Regression.hs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test-extra/transformation/Spec/Regression.hs b/test-extra/transformation/Spec/Regression.hs index e9b51ad4..f374fffe 100644 --- a/test-extra/transformation/Spec/Regression.hs +++ b/test-extra/transformation/Spec/Regression.hs @@ -235,13 +235,11 @@ noBeamsFixture = "examples/regression_jbeam/no-beams-repro.jbeam" noBeamsSpec :: Spec noBeamsSpec = describe "a file with no beams section" $ do - it "is transformed, keeping every node" $ - withNoBeams $ \resultNode -> - length (vertexCoordinates resultNode) `shouldBe` 4 + it "is transformed, keeping every node" . withNoBeams $ \resultNode -> + length (vertexCoordinates resultNode) `shouldBe` 4 - it "classifies no node as support" $ - withNoBeams $ \resultNode -> - commentTexts resultNode `shouldNotContain` ["Support nodes"] + it "classifies no node as support" . withNoBeams $ \resultNode -> + commentTexts resultNode `shouldNotContain` ["Support nodes"] where withNoBeams assert = do topNode <- parseJbeamFile noBeamsFixture From 79d9a13f72f758533b57f3f41081e8a78fb7e561 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:04:55 +0200 Subject: [PATCH 4/8] Name the scalar cases instead of catching them with a wildcard The four that cannot hold a comment are right to return nothing, but the wildcard would have returned nothing for a new constructor that can hold one, and the spec would have gone green for having looked in the wrong place. Naming them makes GHC refuse to compile when Node grows. --- test-extra/transformation/Spec/Helpers.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test-extra/transformation/Spec/Helpers.hs b/test-extra/transformation/Spec/Helpers.hs index cfd1be59..3698356a 100644 --- a/test-extra/transformation/Spec/Helpers.hs +++ b/test-extra/transformation/Spec/Helpers.hs @@ -172,4 +172,7 @@ commentTexts node = case node of Array arrayValue -> concatMap commentTexts (V.toList (avNodes arrayValue)) Object objectValue -> concatMap commentTexts (V.toList (ovNodes objectValue)) ObjectKey (key, value) -> commentTexts key ++ commentTexts value - _ -> [] + String _ -> [] + Number _ -> [] + Bool _ -> [] + Null -> [] From f841c4b92ea376073410cf286fb7704f51ba982c Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:37:00 +0200 Subject: [PATCH 5/8] Ask the connection count directly instead of the comment it produces The spec that looked for a missing "Support nodes" comment could not fail: a vertex absent from the connection map is skipped, so an empty map gives an empty result whatever the fix does. The count vertexConns produces is the thing that actually depends on the beams section, and it returns Left today rather than an empty map. The fixture with three beamed hubs stands next to it, so the negative means something. --- test-extra/transformation/Spec/Helpers.hs | 16 ------ test-extra/transformation/Spec/Regression.hs | 52 +++++++++++++++++--- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/test-extra/transformation/Spec/Helpers.hs b/test-extra/transformation/Spec/Helpers.hs index 3698356a..f9142982 100644 --- a/test-extra/transformation/Spec/Helpers.hs +++ b/test-extra/transformation/Spec/Helpers.hs @@ -7,7 +7,6 @@ module Spec.Helpers ( effectiveMetaByCoordinate, metaNumber, outOfOrderPairs, - commentTexts, ) where import Data.Char (isDigit) @@ -161,18 +160,3 @@ outOfOrderPairs thr resultNode = where positions = zip (vertexPositionsInOrder resultNode) [0 :: Int ..] groupPrefix = T.dropWhileEnd isDigit - -{- | Every comment in a top node, in no particular order. The transformation -writes a side comment above each tree it creates, so the absence of one names -a tree that was never created. --} -commentTexts :: Node -> [Text] -commentTexts node = case node of - Comment comment -> [cText comment] - Array arrayValue -> concatMap commentTexts (V.toList (avNodes arrayValue)) - Object objectValue -> concatMap commentTexts (V.toList (ovNodes objectValue)) - ObjectKey (key, value) -> commentTexts key ++ commentTexts value - String _ -> [] - Number _ -> [] - Bool _ -> [] - Null -> [] diff --git a/test-extra/transformation/Spec/Regression.hs b/test-extra/transformation/Spec/Regression.hs index f374fffe..1096e207 100644 --- a/test-extra/transformation/Spec/Regression.hs +++ b/test-extra/transformation/Spec/Regression.hs @@ -13,11 +13,26 @@ module Spec.Regression ( noBeamsSpec, ) where +import Data.List (sort) +import Data.List.NonEmpty qualified as NE import Data.Map qualified as M import Data.Set qualified as S +import Data.Text (Text) import Data.Text qualified as T +import GHC.IsList (toList) +import JbeamEdit.Core.Node (Node) import JbeamEdit.Transformation +import JbeamEdit.Transformation.BeamExtraction (vertexConns) import JbeamEdit.Transformation.Config +import JbeamEdit.Transformation.Types ( + AnnotatedVertex (..), + VertexTree (..), + ) +import JbeamEdit.Transformation.VertexExtraction ( + determineGroup', + getVertexForest, + verticesQuery, + ) import Spec.Helpers import Test.Hspec @@ -235,14 +250,35 @@ noBeamsFixture = "examples/regression_jbeam/no-beams-repro.jbeam" noBeamsSpec :: Spec noBeamsSpec = describe "a file with no beams section" $ do - it "is transformed, keeping every node" . withNoBeams $ \resultNode -> - length (vertexCoordinates resultNode) `shouldBe` 4 - - it "classifies no node as support" . withNoBeams $ \resultNode -> - commentTexts resultNode `shouldNotContain` ["Support nodes"] - where - withNoBeams assert = do + it "is transformed, keeping every node" $ do topNode <- parseJbeamFile noBeamsFixture case transform M.empty newTransformationConfig topNode of Left err -> expectationFailure ("transform failed: " ++ T.unpack err) - Right (_, _, _, resultNode) -> assert resultNode + Right (_, _, _, resultNode) -> + length (vertexCoordinates resultNode) `shouldBe` 4 + + it "counts a connection for every beamed vertex when there are beams" $ do + topNode <- parseJbeamFile supportRenameIdempotencyFixture + connectionCounts topNode + `shouldBe` Right [("nl0", 3), ("nl10", 3), ("nl20", 3)] + + it "counts nothing at all when there are none" $ do + topNode <- parseJbeamFile noBeamsFixture + connectionCounts topNode `shouldBe` Right [] + +{- | The connection count `vertexConns` produces, as a sorted list so a spec +can read it. Grouping the vertices by tree type is what `transform` does +before it asks, and is repeated here because the wrapper it uses is internal. +-} +connectionCounts :: Node -> Either Text [(Text, Int)] +connectionCounts topNode = do + (_, _, forest) <- getVertexForest brks verticesQuery topNode + let annotated = + concatMap (concatMap (NE.toList . tAnnotatedVertices . snd) . toList) forest + grouped <- M.fromListWith (++) <$> mapM withGroup annotated + (_, conns) <- + vertexConns (maxSupportCoordinates newTransformationConfig) topNode grouped + pure (sort [(name, count) | (name, (_, count)) <- M.toList conns]) + where + brks = xGroupBreakpoints newTransformationConfig + withGroup av = (,[av]) <$> determineGroup' brks (aVertex av) From 81035746867c2414be101ba4520a84986bf2354b Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:39:25 +0200 Subject: [PATCH 6/8] Cut the line the sort below already says --- test-extra/transformation/Spec/Regression.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-extra/transformation/Spec/Regression.hs b/test-extra/transformation/Spec/Regression.hs index 1096e207..ad5d294a 100644 --- a/test-extra/transformation/Spec/Regression.hs +++ b/test-extra/transformation/Spec/Regression.hs @@ -266,9 +266,9 @@ noBeamsSpec = topNode <- parseJbeamFile noBeamsFixture connectionCounts topNode `shouldBe` Right [] -{- | The connection count `vertexConns` produces, as a sorted list so a spec -can read it. Grouping the vertices by tree type is what `transform` does -before it asks, and is repeated here because the wrapper it uses is internal. +{- | Grouping the vertices by tree type is what `transform` does before it +asks for the counts, and is repeated here because the wrapper it uses is +internal. -} connectionCounts :: Node -> Either Text [(Text, Int)] connectionCounts topNode = do From 8040eb461f0fb9d22eaab558bef1b0e8c63a856f Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:52:31 +0200 Subject: [PATCH 7/8] Refactored beam extraction for support node classifaction --- .../transformation/JbeamEdit/Transformation.hs | 6 +++--- .../JbeamEdit/Transformation/BeamExtraction.hs | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src-extra/transformation/JbeamEdit/Transformation.hs b/src-extra/transformation/JbeamEdit/Transformation.hs index 93299460..5a13be46 100644 --- a/src-extra/transformation/JbeamEdit/Transformation.hs +++ b/src-extra/transformation/JbeamEdit/Transformation.hs @@ -227,9 +227,9 @@ moveVerticesInVertexForest triangleVertexNames topNode newNames tfCfg vertexTree Right movableVertices' -> let groupedVertices = M.fromListWith (++) movableVertices' in do - (badBeamNodes, conns) <- - vertexConns (maxSupportCoordinates tfCfg) topNode groupedVertices - let (supportForest, nonSupportVertices) = + let (badBeamNodes, conns) = + vertexConns (maxSupportCoordinates tfCfg) topNode groupedVertices + (supportForest, nonSupportVertices) = moveSupportVertices triangleVertexNames newNames tfCfg conns groupedVertices newForest <- foldM diff --git a/src-extra/transformation/JbeamEdit/Transformation/BeamExtraction.hs b/src-extra/transformation/JbeamEdit/Transformation/BeamExtraction.hs index a39d4957..42bd1a9d 100644 --- a/src-extra/transformation/JbeamEdit/Transformation/BeamExtraction.hs +++ b/src-extra/transformation/JbeamEdit/Transformation/BeamExtraction.hs @@ -1,5 +1,7 @@ module JbeamEdit.Transformation.BeamExtraction (vertexConns, possiblyBeam, extractBeams, extractBeamsWithMeta, beamInKnownSet) where +import Data.Bool (bool) +import Data.Either (fromRight) import Data.List (genericTake, sortOn) import Data.Map (Map) import Data.Map qualified as M @@ -49,10 +51,11 @@ extractBeamFromArray sectionMeta vec maybeObject n@(Object _) = Just n maybeObject _ = Nothing -extractBeams :: Node -> Either Text (Vector Node) +extractBeams :: Node -> Vector Node extractBeams topNode = - NP.queryNodes beamQuery topNode - >>= NP.expectArray beamQuery + fromRight + V.empty + (NP.queryNodes beamQuery topNode >>= NP.expectArray beamQuery) extractBeamsWithMeta :: Vector Node -> ([Node], [Beam]) extractBeamsWithMeta = go M.empty [] [] . V.toList @@ -73,9 +76,8 @@ vertexConns :: Natural -> Node -> Map VertexTreeType [AnnotatedVertex] - -> Either Text ([Node], VertexConnMap) -vertexConns maxSupport topNode vsPerType = - go <$> extractBeams topNode + -> ([Node], VertexConnMap) +vertexConns maxSupport topNode vsPerType = go (extractBeams topNode) where knownNodeNames = S.fromList $ concatMap (map anVertexName) vsPerType go beamNodes = @@ -103,7 +105,7 @@ vertexConns maxSupport topNode vsPerType = | (t, vs) <- M.toList topVerticesPerType , (v, c) <- vs ] - in (badNodes, vertexConnMap) + in bool (badNodes, vertexConnMap) (badNodes, M.empty) (null beamNodes) beamInKnownSet :: Set Text -> Beam -> Bool beamInKnownSet known (Beam (BeamPair a b) _) = From 201785e8e14de1e511c50cab03d9e51cfbac850f Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:15:58 +0200 Subject: [PATCH 8/8] Removed redudant imports --- test-extra/transformation/Spec/Helpers.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test-extra/transformation/Spec/Helpers.hs b/test-extra/transformation/Spec/Helpers.hs index f1ebd1c4..31b472d0 100644 --- a/test-extra/transformation/Spec/Helpers.hs +++ b/test-extra/transformation/Spec/Helpers.hs @@ -19,12 +19,9 @@ import Data.Text qualified as T import Data.Vector qualified as V import GHC.IsList (fromList) import JbeamEdit.Core.Node ( - InternalComment (..), Node (..), NumberValue (..), - avNodes, expectArray, - ovNodes, ) import JbeamEdit.Core.NodePath qualified as NP import JbeamEdit.IOUtils (tryReadFile)