Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public interface DetectionNode {
NodeEntity getGroupByNode();
NodeEntity getRangeNode();
String getFingerprintFilter();
void setFingerprintFilter(String fingerprintFilter);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
49 changes: 36 additions & 13 deletions src/main/java/io/hyperfoil/tools/h5m/svc/NodeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,23 @@ public long update(NodeEntity node){
}else{
NodeEntity existing = NodeEntity.findById(node.id);
if(!existing.name.equals(node.name)){
List<NodeEntity> 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<String,String> 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);
Comment thread
stalep marked this conversation as resolved.
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);
Expand Down Expand Up @@ -490,7 +500,7 @@ public List<ValueEntity> calculateRelativeDifferenceValues(RelativeDifference re
String fpFilter = relDiff.getFingerprintFilter();
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;
}
if (relDiff.getDomainNode() != null) {
Expand Down Expand Up @@ -669,7 +679,7 @@ public List<ValueEntity> 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;
}

Expand Down Expand Up @@ -762,7 +772,7 @@ public List<ValueEntity> 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;
}

Expand Down Expand Up @@ -893,7 +903,7 @@ public List<ValueEntity> 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;
}

Expand Down Expand Up @@ -1119,7 +1129,7 @@ public static String renameParameters(String function,Map<String,String> 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
Expand Down Expand Up @@ -1276,7 +1286,7 @@ public List<ValueEntity> calculateFpValues(FingerprintNode node, Map<Long, Value
TreeMap<String, JqValue> 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);
Expand All @@ -1287,10 +1297,23 @@ public List<ValueEntity> calculateFpValues(FingerprintNode node, Map<Long, Value
newValue.sources = node.sources.stream().filter(n->sourceValues.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)
Expand Down
5 changes: 3 additions & 2 deletions src/test/java/io/hyperfoil/tools/h5m/cli/H5mTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
126 changes: 111 additions & 15 deletions src/test/java/io/hyperfoil/tools/h5m/svc/NodeServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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");

}



Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ValueEntity> 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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String> 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<keys.size(); i++) {
assertEquals(1,keys.get(i).compareTo(keys.get(i-1)));
}
}

@Test
public void evaluateFingerprintFilter_matching() throws IOException {
JqValue fingerprint = JqValues.parse("{\"platform\":\"x86\",\"buildType\":\"release\"}");
boolean result = nodeService.evaluateFingerprintFilter("(fp) => 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");
}

Expand Down Expand Up @@ -1604,10 +1699,11 @@ public void calculateFpValues_with_qvss_data() throws IOException, SystemExcepti

// verify sorted keys and real data values
List<String> 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<keys.size(); i++) {
assertEquals(1,keys.get(i).compareTo(keys.get(i-1)));
}
assertEquals("22.3.r17-grl", fpObject.get(""+javaVersionNode.id).asText());
assertEquals("3.0.0.Alpha5", fpObject.get(""+quarkusVersionNode.id).asText());
}

@Test
Expand Down
Loading