Skip to content

Commit 8d6bdc5

Browse files
authored
fix: branch distance (#153)
Improves the string distance calculation by using the real-coded string edit distance. Fixes the multi clause conditions and the unary not operator
1 parent c6381f2 commit 8d6bdc5

15 files changed

+950
-147
lines changed

libraries/search-javascript/lib/criterion/BranchDistance.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import { transformSync, traverse } from "@babel/core";
2525
import { defaultBabelOptions } from "@syntest/analysis-javascript";
2626

2727
export class BranchDistance extends CoreBranchDistance {
28+
protected stringAlphabet: string;
29+
30+
constructor(stringAlphabet: string) {
31+
super();
32+
this.stringAlphabet = stringAlphabet;
33+
}
34+
2835
calculate(
2936
_conditionAST: string, // deprecated
3037
condition: string,
@@ -37,10 +44,14 @@ export class BranchDistance extends CoreBranchDistance {
3744
const options: unknown = JSON.parse(JSON.stringify(defaultBabelOptions));
3845

3946
const ast = transformSync(condition, options).ast;
40-
const visitor = new BranchDistanceVisitor(variables, !trueOrFalse);
47+
const visitor = new BranchDistanceVisitor(
48+
this.stringAlphabet,
49+
variables,
50+
!trueOrFalse
51+
);
4152

4253
traverse(ast, visitor);
43-
const distance = visitor._getDistance(condition);
54+
let distance = visitor._getDistance(condition);
4455

4556
if (distance > 1 || distance < 0) {
4657
throw new Error("Invalid distance!");
@@ -50,6 +61,11 @@ export class BranchDistance extends CoreBranchDistance {
5061
throw new TypeError(shouldNeverHappen("BranchDistance"));
5162
}
5263

64+
if (distance === 1) {
65+
// We dont want a branch distance of 1 because then it will be equal to covering the oposite branch
66+
distance = 0.999_999_999_999_999_9;
67+
}
68+
5369
return distance;
5470
}
5571
}

libraries/search-javascript/lib/criterion/BranchDistanceVisitor.ts

Lines changed: 195 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { shouldNeverHappen } from "@syntest/search";
2525
export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
2626
protected static override LOGGER: Logger;
2727

28+
protected _stringAlphabet: string;
29+
2830
private _K = 1; // punishment factor
2931

3032
private _variables: Record<string, unknown>;
@@ -34,8 +36,13 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
3436
private _isDistanceMap: Map<string, boolean>;
3537
private _distance: number;
3638

37-
constructor(variables: Record<string, unknown>, inverted: boolean) {
39+
constructor(
40+
stringAlphabet: string,
41+
variables: Record<string, unknown>,
42+
inverted: boolean
43+
) {
3844
super("");
45+
this._stringAlphabet = stringAlphabet;
3946
this._variables = variables;
4047
this._inverted = inverted;
4148

@@ -282,22 +289,30 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
282289
}
283290
case "!": {
284291
if (argumentIsDistance) {
285-
value = this._inverted ? 1 - argumentValue : argumentValue;
292+
value = this._inverted
293+
? this._normalize(1 - argumentValue)
294+
: this._normalize(argumentValue);
286295
} else {
287296
if (this._inverted) {
288-
value =
289-
typeof argumentValue === "number"
290-
? this._normalize(Math.abs(0 - argumentValue))
291-
: argumentValue
292-
? 0
293-
: this._normalize(1);
297+
if (typeof argumentValue === "boolean") {
298+
value = argumentValue ? 0 : this._normalize(1);
299+
} else if (typeof argumentValue === "number") {
300+
value = argumentValue ? 0 : this._normalize(1);
301+
} else {
302+
// could be other type
303+
value = argumentValue ? 0 : this._normalize(Number.MAX_VALUE);
304+
}
294305
} else {
295-
value =
296-
typeof argumentValue === "number"
306+
if (typeof argumentValue === "boolean") {
307+
value = argumentValue ? this._normalize(1) : 0;
308+
} else if (typeof argumentValue === "number") {
309+
value = argumentValue
297310
? this._normalize(Math.abs(0 - argumentValue))
298-
: argumentValue
299-
? this._normalize(1)
300311
: 0;
312+
} else {
313+
// could be other type
314+
value = argumentValue ? this._normalize(Number.MAX_VALUE) : 0;
315+
}
301316
}
302317
}
303318
break;
@@ -457,7 +472,7 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
457472
typeof leftValue === "string" &&
458473
typeof rightValue === "string"
459474
) {
460-
value = this._editDistDP(leftValue, rightValue);
475+
value = this._realCodedEditDistance(leftValue, rightValue);
461476
} else if (
462477
typeof leftValue === "boolean" &&
463478
typeof rightValue === "boolean"
@@ -466,9 +481,9 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
466481
} else {
467482
// TODO type difference?!
468483
if (operator === "===") {
469-
value = leftValue === rightValue ? 0 : 1;
484+
value = leftValue === rightValue ? 0 : Number.MAX_VALUE;
470485
} else {
471-
value = leftValue == rightValue ? 0 : 1;
486+
value = leftValue == rightValue ? 0 : Number.MAX_VALUE;
472487
}
473488
}
474489
break;
@@ -488,18 +503,18 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
488503
value = 1; // TODO should this one be inverted?
489504
} else {
490505
if (this._inverted) {
491-
value = leftValue in rightValue ? 1 : 0;
506+
value = leftValue in rightValue ? Number.MAX_VALUE : 0;
492507
} else {
493-
value = leftValue in rightValue ? 0 : 1;
508+
value = leftValue in rightValue ? 0 : Number.MAX_VALUE;
494509
}
495510
}
496511
break;
497512
}
498513
case "instanceof": {
499514
if (this._inverted) {
500-
value = leftValue instanceof rightValue ? 1 : 0;
515+
value = leftValue instanceof rightValue ? Number.MAX_VALUE : 0;
501516
} else {
502-
value = leftValue instanceof rightValue ? 0 : 1;
517+
value = leftValue instanceof rightValue ? 0 : Number.MAX_VALUE;
503518
}
504519
break;
505520
}
@@ -582,36 +597,82 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
582597
public LogicalExpression: (path: NodePath<t.LogicalExpression>) => void = (
583598
path
584599
) => {
600+
let operator = path.node.operator;
601+
585602
const left = path.get("left");
586603
const right = path.get("right");
587604

605+
if (this._inverted) {
606+
if (operator === "||") {
607+
operator = "&&";
608+
} else if (operator === "&&") {
609+
operator = "||";
610+
}
611+
}
612+
588613
left.visit();
589614
right.visit();
590615

591616
let leftValue = <any>this._valueMap.get(left.toString());
592617
let rightValue = <any>this._valueMap.get(right.toString());
593618

594619
if (!this._isDistanceMap.get(left.toString())) {
595-
leftValue = leftValue ? 0 : 1;
620+
// should we check for number /booleans here?
621+
if (this._inverted) {
622+
if (typeof leftValue === "boolean") {
623+
leftValue = leftValue ? this._normalize(1) : 0;
624+
} else if (typeof leftValue === "number") {
625+
leftValue = leftValue ? this._normalize(Math.abs(0 - leftValue)) : 0;
626+
} else {
627+
leftValue = leftValue ? this._normalize(Number.MAX_VALUE) : 0;
628+
}
629+
} else {
630+
if (typeof leftValue === "boolean") {
631+
leftValue = leftValue ? 0 : this._normalize(1);
632+
} else if (typeof leftValue === "number") {
633+
leftValue = leftValue ? 0 : this._normalize(1);
634+
} else {
635+
leftValue = leftValue ? 0 : this._normalize(Number.MAX_VALUE);
636+
}
637+
}
596638
}
597639

598640
if (!this._isDistanceMap.get(right.toString())) {
599-
rightValue = rightValue ? 0 : 1;
641+
// should we check for number /booleans here?
642+
if (this._inverted) {
643+
if (typeof rightValue === "boolean") {
644+
rightValue = rightValue ? this._normalize(1) : 0;
645+
} else if (typeof rightValue === "number") {
646+
rightValue = rightValue
647+
? this._normalize(Math.abs(0 - rightValue))
648+
: 0;
649+
} else {
650+
rightValue = rightValue ? this._normalize(Number.MAX_VALUE) : 0;
651+
}
652+
} else {
653+
if (typeof rightValue === "boolean") {
654+
rightValue = rightValue ? 0 : this._normalize(1);
655+
} else if (typeof rightValue === "number") {
656+
rightValue = rightValue ? 0 : this._normalize(1);
657+
} else {
658+
rightValue = rightValue ? 0 : this._normalize(Number.MAX_VALUE);
659+
}
660+
}
600661
}
601662

602663
let value: unknown;
603-
switch (path.node.operator) {
664+
switch (operator) {
604665
case "||": {
605-
value = this._normalize(Math.min(leftValue, rightValue));
666+
value = Math.min(leftValue, rightValue); // should this be normalized?
606667
break;
607668
}
608669
case "&&": {
609-
value = this._normalize(leftValue + rightValue);
670+
value = this._normalize(leftValue + rightValue); // should this be normalized?
610671
break;
611672
}
612673
case "??": {
613674
// TODO no clue
614-
value = 0;
675+
value = this._normalize(Number.MAX_VALUE);
615676
break;
616677
}
617678
default: {
@@ -626,7 +687,115 @@ export class BranchDistanceVisitor extends AbstractSyntaxTreeVisitor {
626687
path.skip();
627688
};
628689

629-
private _editDistDP(string1: string, string2: string) {
690+
private minimum(a: number, b: number, c: number) {
691+
let mi;
692+
693+
mi = a;
694+
if (b < mi) {
695+
mi = b;
696+
}
697+
698+
if (c < mi) {
699+
mi = c;
700+
}
701+
return mi;
702+
}
703+
704+
protected _realCodedEditDistance(s: string, t: string) {
705+
const d: number[][] = []; // matrix
706+
let index; // iterates through s
707+
let index_; // iterates through t
708+
let s_index; // ith character of s
709+
let t_index; // jth character of t
710+
let cost; // cost
711+
712+
if (s == undefined && t != undefined) {
713+
return t.length;
714+
}
715+
if (t == undefined && s != undefined) {
716+
return s.length;
717+
}
718+
if (s == undefined && t == undefined) {
719+
return Number.MAX_VALUE;
720+
}
721+
// Step 1
722+
723+
const n = s.length; // length of s
724+
const m = t.length; // length of t
725+
if (n == 0) {
726+
return m;
727+
}
728+
if (m == 0) {
729+
return n;
730+
}
731+
732+
for (let indexA = 0; indexA < n + 1; indexA++) {
733+
const row = [];
734+
for (let indexB = 0; indexB < m + 1; indexB++) {
735+
row.push(0);
736+
}
737+
d.push(row);
738+
}
739+
740+
// Step 2
741+
742+
for (index = 0; index <= n; index++) {
743+
d[index][0] = index;
744+
}
745+
746+
for (index_ = 0; index_ <= m; index_++) {
747+
d[0][index_] = index_;
748+
}
749+
750+
// Step 3
751+
752+
for (index = 1; index <= n; index++) {
753+
s_index = s.charAt(index - 1);
754+
755+
// Step 4
756+
757+
for (index_ = 1; index_ <= m; index_++) {
758+
t_index = t.charAt(index_ - 1);
759+
760+
// Step 5
761+
762+
if (s_index == t_index) {
763+
cost = 0;
764+
} else {
765+
//
766+
if (
767+
!this._stringAlphabet.includes(t_index) ||
768+
!this._stringAlphabet.includes(s_index)
769+
) {
770+
BranchDistanceVisitor.LOGGER.warn(
771+
`cannot search for character missing from the sampling alphabet one of these is missing: ${t_index}, ${s_index}`
772+
);
773+
cost = Number.MAX_VALUE;
774+
} else {
775+
cost = Math.abs(
776+
this._stringAlphabet.indexOf(s_index) -
777+
this._stringAlphabet.indexOf(t_index)
778+
);
779+
}
780+
cost = this._normalize(cost);
781+
}
782+
783+
// Step 6
784+
785+
d[index][index_] = this.minimum(
786+
d[index - 1][index_] + 1,
787+
d[index][index_ - 1] + 1,
788+
d[index - 1][index_ - 1] + cost
789+
);
790+
}
791+
}
792+
793+
// Step 7
794+
795+
return d[n][m];
796+
}
797+
798+
protected _editDistDP(string1: string, string2: string) {
630799
const m = string1.length;
631800
const n = string2.length;
632801
const table = [];

libraries/search-javascript/lib/search/JavaScriptSubject.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,24 @@ import { JavaScriptTestCase } from "../testcase/JavaScriptTestCase";
3131
import { BranchDistance } from "../criterion/BranchDistance";
3232

3333
export class JavaScriptSubject extends SearchSubject<JavaScriptTestCase> {
34-
constructor(target: Target, rootContext: RootContext) {
34+
protected stringAlphabet: string;
35+
constructor(
36+
target: Target,
37+
rootContext: RootContext,
38+
stringAlphabet: string
39+
) {
3540
super(target, rootContext);
41+
this.stringAlphabet = stringAlphabet;
42+
43+
this._extractObjectives();
3644
}
3745

3846
protected _extractObjectives(): void {
47+
this._objectives = new Map<
48+
ObjectiveFunction<JavaScriptTestCase>,
49+
ObjectiveFunction<JavaScriptTestCase>[]
50+
>();
51+
3952
const functions = this._rootContext.getControlFlowProgram(
4053
this._target.path
4154
).functions;
@@ -63,7 +76,7 @@ export class JavaScriptSubject extends SearchSubject<JavaScriptTestCase> {
6376
this._objectives.set(
6477
new BranchObjectiveFunction(
6578
new ApproachLevel(),
66-
new BranchDistance(),
79+
new BranchDistance(this.stringAlphabet),
6780
this,
6881
edge.target
6982
),

0 commit comments

Comments
 (0)