diff --git a/src/main/java/io/hyperfoil/tools/h5m/entity/node/DetectionNode.java b/src/main/java/io/hyperfoil/tools/h5m/entity/node/DetectionNode.java index 4c4c829d..ca069899 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/entity/node/DetectionNode.java +++ b/src/main/java/io/hyperfoil/tools/h5m/entity/node/DetectionNode.java @@ -11,4 +11,5 @@ public interface DetectionNode { NodeEntity getGroupByNode(); NodeEntity getRangeNode(); String getFingerprintFilter(); + void setFingerprintFilter(String fingerprintFilter); } diff --git a/src/main/java/io/hyperfoil/tools/h5m/entity/node/EDivisive.java b/src/main/java/io/hyperfoil/tools/h5m/entity/node/EDivisive.java index 993bad94..c9d45ef8 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/entity/node/EDivisive.java +++ b/src/main/java/io/hyperfoil/tools/h5m/entity/node/EDivisive.java @@ -152,6 +152,7 @@ public String getFingerprintFilter() { return config.getString(FINGERPRINT_FILTER); } + @Override public void setFingerprintFilter(String fingerprintFilter) { config.set(FINGERPRINT_FILTER, fingerprintFilter); operation = config.toString(); diff --git a/src/main/java/io/hyperfoil/tools/h5m/entity/node/FixedThreshold.java b/src/main/java/io/hyperfoil/tools/h5m/entity/node/FixedThreshold.java index ab6f9cfa..93a079f9 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/entity/node/FixedThreshold.java +++ b/src/main/java/io/hyperfoil/tools/h5m/entity/node/FixedThreshold.java @@ -152,7 +152,7 @@ public void setMaxInclusive(boolean maxInclusive) { public String getFingerprintFilter() { return config.has(FINGERPRINT_FILTER) ? config.get(FINGERPRINT_FILTER).asString(null) : null; } - + @Override public void setFingerprintFilter(String fingerprintFilter) { config = config.with(FINGERPRINT_FILTER, io.hyperfoil.tools.jjq.value.JqString.of(fingerprintFilter)); operation = config.toJsonString(); diff --git a/src/main/java/io/hyperfoil/tools/h5m/entity/node/RelativeDifference.java b/src/main/java/io/hyperfoil/tools/h5m/entity/node/RelativeDifference.java index 9375676a..1d1ed285 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/entity/node/RelativeDifference.java +++ b/src/main/java/io/hyperfoil/tools/h5m/entity/node/RelativeDifference.java @@ -139,6 +139,7 @@ public void setFilter(String filter){ public String getFingerprintFilter(){ return config.has(FINGERPRINT_FILTER) ? config.get(FINGERPRINT_FILTER).asString(null) : null; } + @Override public void setFingerprintFilter(String fingerprintFilter){ config = config.with(FINGERPRINT_FILTER, JqString.of(fingerprintFilter)); operation = config.toJsonString(); diff --git a/src/main/java/io/hyperfoil/tools/h5m/entity/node/StdDevAnomaly.java b/src/main/java/io/hyperfoil/tools/h5m/entity/node/StdDevAnomaly.java index 7a9018fc..4192b59f 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/entity/node/StdDevAnomaly.java +++ b/src/main/java/io/hyperfoil/tools/h5m/entity/node/StdDevAnomaly.java @@ -174,6 +174,7 @@ public String getFingerprintFilter() { return config.getString(FINGERPRINT_FILTER); } + @Override public void setFingerprintFilter(String fingerprintFilter) { config.set(FINGERPRINT_FILTER, fingerprintFilter); operation = config.toString(); diff --git a/src/main/java/io/hyperfoil/tools/h5m/svc/NodeService.java b/src/main/java/io/hyperfoil/tools/h5m/svc/NodeService.java index 7f60d2d0..5bfdc80a 100644 --- a/src/main/java/io/hyperfoil/tools/h5m/svc/NodeService.java +++ b/src/main/java/io/hyperfoil/tools/h5m/svc/NodeService.java @@ -170,13 +170,23 @@ public long update(NodeEntity node){ }else{ NodeEntity existing = NodeEntity.findById(node.id); if(!existing.name.equals(node.name)){ - List toChange = em.createNativeQuery( - "select n.* from node n join node_edge ne on n.id = ne.child_id where ne.parent_id=? and n.type='ecma'" - , NodeEntity.class).setParameter(1,node.id).getResultList(); Map changes = Map.of(existing.name,node.name); - for(NodeEntity n : toChange){ - n.operation = renameParameters(n.operation, changes); - em.merge(n); + //change ecma + for(NodeEntity dependent : getDependentNodes(existing)){ + if(dependent.type().equals(NodeType.JS)){ + dependent.operation = renameParameters(dependent.operation,changes); + em.merge(dependent); + }else if (dependent.type().equals(NodeType.FINGERPRINT)){ + //the name passed to the fingerprint changed so we check for filters + for(NodeEntity fingerprintDependent : getDependentNodes(dependent)){ + if(fingerprintDependent instanceof DetectionNode detectionNode){ + String newFilter = renameParameters(detectionNode.getFingerprintFilter(),changes); + detectionNode.setFingerprintFilter(newFilter); + em.merge(detectionNode); + } + } + } + //not checking for direct DetectionNode dependency because that would not result in filters operating on value keys } } em.merge(node); @@ -490,7 +500,7 @@ public List calculateRelativeDifferenceValues(RelativeDifference re String fpFilter = relDiff.getFingerprintFilter(); for(int fIdx=0; fIdx calculateFixedThresholdValues(FixedThreshold ft, ValueE for (int fIdx = 0; fIdx < fingerprintValues.size(); fIdx++) { ValueEntity fingerprintValue = fingerprintValues.get(fIdx); - if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data)) { + if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data,fingerprintValue.node)) { continue; } @@ -762,7 +772,7 @@ public List calculateEDivisiveValues(EDivisive ed, ValueEntity root int maxSeriesLength = ed.getMaxSeriesLength(); for (int fIdx = 0; fIdx < fingerprintValues.size(); fIdx++) { ValueEntity fingerprintValue = fingerprintValues.get(fIdx); - if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data)) { + if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data,fingerprintValue.node)) { continue; } @@ -893,7 +903,7 @@ public List calculateStdDevAnomalyValues(StdDevAnomaly sd, ValueEnt for (int fIdx = 0; fIdx < fingerprintValues.size(); fIdx++) { ValueEntity fingerprintValue = fingerprintValues.get(fIdx); - if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data)) { + if (fpFilter != null && !evaluateFingerprintFilter(fpFilter, fingerprintValue.data,fingerprintValue.node)) { continue; } @@ -1119,7 +1129,7 @@ public static String renameParameters(String function,Map renames if( previous.matches("[a-zA-Z_\\$]") || //part of another name following.matches("[a-zA-Z_\\$0-9]") || //part of another name - previous.equals(".") || //method call + (previous.equals(".") && following.equals("(")) || //method call (following.equals(":") && !previous.equals("?")) // key in an object, not a tertiary expression ){ //skip it @@ -1276,7 +1286,7 @@ public List calculateFpValues(FingerprintNode node, Map sorted = new TreeMap<>(); for (NodeEntity source : node.sources) { if (sourceValues.containsKey(source.getId())) { - sorted.put(source.name, sourceValues.get(source.getId()).data); + sorted.put(""+source.id, sourceValues.get(source.getId()).data); } } sorted.forEach(fpBuilder::put); @@ -1287,10 +1297,23 @@ public List calculateFpValues(FingerprintNode node, MapsourceValues.containsKey(n.getId())).map(n -> sourceValues.get(n.getId())).collect(Collectors.toList()); return List.of(newValue); } - public boolean evaluateFingerprintFilter(String filter, JqValue fingerprint) { + public JqValue createNamedFingerprintValue(NodeEntity node,JqObject value){ + if(! (node instanceof FingerprintNode)){ + return value; + } + JqObject.Builder builder = JqObject.builder(); + for(NodeEntity source : node.sources){ + if(value.has(""+source.id)){ + builder.put(source.name,value.get(""+source.id)); + } + } + return builder.build(); + } + public boolean evaluateFingerprintFilter(String filter, JqValue fingerprint, NodeEntity node) { if (filter == null || filter.isBlank()) { return true; } + fingerprint = fingerprint.isObject() && node!=null ? createNamedFingerprintValue(node,(JqObject) fingerprint) : fingerprint; try (Context context = Context.newBuilder("js") .engine(JS_ENGINE) .allowExperimentalOptions(true) diff --git a/src/test/java/io/hyperfoil/tools/h5m/cli/H5mTest.java b/src/test/java/io/hyperfoil/tools/h5m/cli/H5mTest.java index b3daf595..8104cbbe 100644 --- a/src/test/java/io/hyperfoil/tools/h5m/cli/H5mTest.java +++ b/src/test/java/io/hyperfoil/tools/h5m/cli/H5mTest.java @@ -1035,12 +1035,13 @@ public void fixedthreshold_qvss_split_by_framework() { // All spring-jvm values (~9400-12200) are below min=15000 assertTrue(output.contains("below"), "should detect spring below threshold\n" + output); - assertTrue(output.contains("\"fwName\":\"spring-jvm\""), + assertTrue(output.contains(":\"spring-jvm\""), "should have spring-jvm in violation fingerprint\n" + output); // All quarkus-jvm values (~28700-37400) are above min=15000 // No violation should contain quarkus-jvm fingerprint — scoping must keep them separate - assertFalse(output.contains("\"fingerprint\":{\"fwName\":\"quarkus-jvm\"}"), + boolean found = output.lines().anyMatch(line->line.contains("finterprint") && line.contains("quarkus-jvm")); + assertFalse(found, "quarkus-jvm should not appear in violations — scoping broken\n" + output); } diff --git a/src/test/java/io/hyperfoil/tools/h5m/svc/NodeServiceTest.java b/src/test/java/io/hyperfoil/tools/h5m/svc/NodeServiceTest.java index 295acd39..7edb675b 100644 --- a/src/test/java/io/hyperfoil/tools/h5m/svc/NodeServiceTest.java +++ b/src/test/java/io/hyperfoil/tools/h5m/svc/NodeServiceTest.java @@ -291,6 +291,19 @@ public void renameParameter_skip_object_key(){ public void renameParameter_tertiary_refernece(){ assertEquals("(_a,_b,_c)=> _a ? _b: _c",nodeService.renameParameters("(a,b,c)=> a ? b: c",Map.of("a","_a","b","_b","c","_c"))); } + @Test + public void renameParameter_filter_miss(){ + assertEquals("value => value === \"true\"",nodeService.renameParameters("value => value === \"true\"",Map.of("before","after"))); + } + @Test + public void renameParameter_filter_array_access(){ + assertEquals("value => (value[\"Fun ID\"].match(/^gitlab-ci-nightly/))",nodeService.renameParameters("value => (value[\"Run ID\"].match(/^gitlab-ci-nightly/))",Map.of("Run ID","Fun ID"))); + } + @Test + public void renameParameter_filter_property_access(){ + assertEquals("value => value.after",nodeService.renameParameters("value => value.before",Map.of("before","after"))); + } + @Test public void update_changes_javascript_argument_name() throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException { @@ -308,6 +321,34 @@ public void update_changes_javascript_argument_name() throws SystemException, No assertEquals("(newName)=>newName", found.operation,"the change should update method"); } + @Test + public void update_changes_detection_node_filter_through_fingerprint() throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException { + tm.begin(); + NodeEntity root = new RootNode(); + root.persist(); + NodeEntity n = new JqNode("oldName", "operation",root); + n.persist(); + FingerprintNode fingerprintNode = new FingerprintNode("fp","",List.of(n)); + fingerprintNode.persist(); + JqNode range = new JqNode("range",".range",root); + range.persist(); + JqNode domain = new JqNode("domain","domain",root); + domain.persist(); + RelativeDifference detectionNode = new RelativeDifference("detect",""); + detectionNode.setFingerprintFilter("value=>value.oldName == 'foo'"); + detectionNode.setNodes(fingerprintNode,root,range,domain); + detectionNode.persist(); + tm.commit(); + + n.name = "newName"; + + nodeService.update(n); + NodeEntity found = NodeEntity.findById(detectionNode.id); + assertInstanceOf(RelativeDifference.class,found); + RelativeDifference relativeDifference = (RelativeDifference) found; + assertEquals("value=>value.newName == 'foo'", relativeDifference.getFingerprintFilter(),"the change should update method"); + + } @@ -1185,7 +1226,7 @@ public void calculateFpValues_same_name_sources_preserves_all() throws SystemExc // the last value wins in the TreeMap sort. The important thing is that // both values were accessible — not silently lost before reaching the builder. assertNotNull(fpObject, "fingerprint should be built from both sources"); - assertTrue(fpObject.has("platform"), "fingerprint should contain the 'platform' key"); + assertTrue(fpObject.has(""+sourceB.id), "fingerprint should contain the 'platform' node id key"); } @Test @@ -1470,6 +1511,59 @@ public void calculateValues_NxN_complicated() throws SystemException, NotSupport }); } + @Test + public void calculateFpValues_consistent_across_source_mutation() throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException, IOException { + tm.begin(); + NodeEntity rootNode = new RootNode(); + rootNode.persist(); + JqNode aNode = new JqNode("a",".a", rootNode); + aNode.persist(); + JqNode bNode = new JqNode("b",".b", rootNode); + bNode.persist(); + FingerprintNode fp = new FingerprintNode("fp","",List.of(aNode,bNode)); + fp.persist(); + ValueEntity rootValue = new ValueEntity(null,rootNode,JqValues.parse( + """ + { "a" : 1, "b": 1} + """ + )); + rootValue.persist(); + ValueEntity aValue = new ValueEntity(null,aNode,rootValue.data.getField("a")); + aValue.persist(); + ValueEntity bValue = new ValueEntity(null,bNode,rootValue.data.getField("b")); + bValue.persist(); + tm.commit(); + List found = nodeService.calculateFpValues(fp,Map.of(aNode.id,aValue,bNode.id,bValue),0); + assertNotNull(found); + assertEquals(1,found.size()); + JqValue before = found.getFirst().data; + assertNotNull(before); + + //mutate a source + tm.begin(); + NodeEntity readA = nodeService.read(aNode.id); + readA.name="Z"; + readA.persist(); + + ValueEntity readAValue = valueService.byId(aValue.id); + ValueEntity readBValue = valueService.byId(bValue.id); + + NodeEntity readFp = nodeService.read(fp.id); + + found = nodeService.calculateFpValues((FingerprintNode) readFp,Map.of(readA.id,readAValue,bNode.id,readBValue),0); + + tm.commit(); + + + assertNotNull(found); + assertEquals(1,found.size()); + JqValue after = found.getFirst().data; + assertNotNull(after); + + assertEquals(before,after); + + } + @Test public void calculateFpValues_structured_json() throws IOException, SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException { tm.begin(); @@ -1497,10 +1591,10 @@ public void calculateFpValues_structured_json() throws IOException, SystemExcept assertNotNull(fpValue.data); assertTrue(fpValue.data instanceof JqObject, "fingerprint data should be a JqObject, not a hash: " + fpValue.data.getClass()); JqObject fpObject = (JqObject) fpValue.data; - assertTrue(fpObject.has("platform"), "fingerprint should contain 'platform' key"); - assertTrue(fpObject.has("buildType"), "fingerprint should contain 'buildType' key"); - assertEquals("x86", fpObject.get("platform").asText()); - assertEquals("release", fpObject.get("buildType").asText()); + assertTrue(fpObject.has(""+platformNode.id), "fingerprint should contain 'platform' key"); + assertTrue(fpObject.has(""+buildTypeNode.id), "fingerprint should contain 'buildType' key"); + assertEquals("x86", fpObject.get(""+platformNode.id).asText()); + assertEquals("release", fpObject.get(""+buildTypeNode.id).asText()); } @Test @@ -1530,35 +1624,36 @@ public void calculateFpValues_sorted_keys() throws IOException, SystemException, JqObject fpObject = (JqObject) result.getFirst().data; // verify keys are sorted alphabetically: buildType before platform List keys = new ArrayList<>(fpObject.objectValue().keySet()); - assertEquals("buildType", keys.get(0), "first key should be 'buildType' (alphabetical order)"); - assertEquals("platform", keys.get(1), "second key should be 'platform' (alphabetical order)"); + for(int i=1; i fp.platform === \"x86\"", fingerprint); + boolean result = nodeService.evaluateFingerprintFilter("(fp) => fp.platform === \"x86\"", fingerprint,null); assertTrue(result, "filter should match when platform is x86"); } @Test public void evaluateFingerprintFilter_non_matching() throws IOException { JqValue fingerprint = JqValues.parse("{\"platform\":\"x86\",\"buildType\":\"release\"}"); - boolean result = nodeService.evaluateFingerprintFilter("(fp) => fp.platform === \"arm\"", fingerprint); + boolean result = nodeService.evaluateFingerprintFilter("(fp) => fp.platform === \"arm\"", fingerprint,null); assertFalse(result, "filter should not match when platform is x86 but filter expects arm"); } @Test public void evaluateFingerprintFilter_null_passes_all() throws IOException { JqValue fingerprint = JqValues.parse("{\"platform\":\"x86\",\"buildType\":\"release\"}"); - boolean result = nodeService.evaluateFingerprintFilter(null, fingerprint); + boolean result = nodeService.evaluateFingerprintFilter(null, fingerprint,null); assertTrue(result, "null filter should pass all fingerprints"); } @Test public void evaluateFingerprintFilter_compound_filter() throws IOException { JqValue fingerprint = JqValues.parse("{\"platform\":\"x86\",\"buildType\":\"release\"}"); - boolean result = nodeService.evaluateFingerprintFilter("(fp) => fp.platform === \"x86\" && fp.buildType === \"release\"", fingerprint); + boolean result = nodeService.evaluateFingerprintFilter("(fp) => fp.platform === \"x86\" && fp.buildType === \"release\"", fingerprint,null); assertTrue(result, "compound filter should match when both conditions are true"); } @@ -1604,10 +1699,11 @@ public void calculateFpValues_with_qvss_data() throws IOException, SystemExcepti // verify sorted keys and real data values List keys = new ArrayList<>(fpObject.objectValue().keySet()); - assertEquals("JAVA_VERSION", keys.get(0), "first key should be JAVA_VERSION (sorted)"); - assertEquals("QUARKUS_VERSION", keys.get(1), "second key should be QUARKUS_VERSION (sorted)"); - assertEquals("22.3.r17-grl", fpObject.get("JAVA_VERSION").asText()); - assertEquals("3.0.0.Alpha5", fpObject.get("QUARKUS_VERSION").asText()); + for(int i=1; i