Let's run it and see?
Christian Gram Kalhauge
Name cases where the choice of a dynamic analysis makes the most sense?
What are the primary limitation and challenges of dynamic analysis?
When is random sampling of inputs better than testing small values and visa versa?
The three steps of a dynamic analysis, 1.1 we select a trace from the program, 1.2 we try to predict a class of traces from which our trace came from, 1.3 we analyse the set of traces. If we did not find anything we might repeat the analysis 2.1
did it end in a failure?
did any opened resource, not get closed?
did we execute this instruction?
Story: No analysis are truly sound!
int checkTheWrongThing(int a) {
if (a != 0) {
return a / 0;
} else {
return 0;
}
}Instruction coverage vs Path equivalence
How much do we need to save to disk?
Everything done at runtime ... fast and enables reactions.
Everything done at rest ... lazy and enables more analysis.
Every warning is real
Every warning has a trace
Setting up the Environment
Warning by doing
Only tells you what happens
Run the program and see if it crashes.
If the method takes arguments, exit early, otherwise,
Run the case with your interpreter,
Give the case returned 100%, and all others 0%.
Check if the output is the same.
$ uv run jpamb interpret -r out.expected <your_interpreter>Crash the program early!
public static int square(int height, int width) {
assert height > 0;
assert width > 0;
return height * width;
}Negative Space Programming
case Get(field=field):
assert field.fieldid.name == "assertionsDisabled"
frame.stack.push(Value.int(0)) # Push FalseSanitizers = Automatic Assertions
Choose inputs at random, hope to hit all trace classes
void split(int i) {
if (i > 0) {
...
} else {
...
}
}Not all Trace classes are easy to hit
void isEqualToZero(int i) {
if (i == 0) {
...
} else {
...
}
}build a dictionary of interesting values, and
make it more likely to choose those.
A more complicated technique.
@Case("([C: 'h','e','l','l','o']) -> ok")
@Case("([C: 'x']) -> assertion error")
@Case("([C: ]) -> out of bounds")
@Tag({ ARRAY })
public static void arraySpellsHello(char[] array) {
assert array[0] == 'h'
&& array[1] == 'e'
&& array[2] == 'l'
&& array[3] == 'l'
&& array[4] == 'o';
}This case is not easy!
Given a function from byte-strings to program inputs:
pick an interesting test-case.
try to change it by removing, adding or modifying bytes.
convert it to a program input and run it and see if it adds coverage.
if so add it to the interesting cases.
Assertions + Fuzzing = Magic!
from hypothesis import given, strategies as st
@given(st.lists(st.integers() | st.floats()))
def test_sort_correct(lst):
# lst is a random list of numbers
assert my_sort(lst) == sorted(lst)
test_sort_correct()Most bugs in a program can be found by investigating only a small section of the inputs.
void isEqualToZero(int i) {
if (i == 0) {
...
} else {
...
}
}void isEqualToAMillion(int i) {
if (i == 1000000) {
...
} else {
...
}
}We can create small-check generators
def gen_int(depth):
yield 0
for i in range(depth):
yield (i + 1)
yield -(i + 1)Call using iterative deepening gen_int(0), gen_int(1), gen_int(2)
Name cases where the choice of a dynamic analysis makes the most sense?
What are the primary limitation and challenges of dynamic analysis?
When is random sampling of inputs better than testing small values and visa versa?