Introduction
Tests are split into language and environment. That is, the language being tested, and the environment running the test code.
Curriculum Helpers
RandomMocker
Mocks Math.random for testing purposes. Each time mock()
is called the pseudo-random number generator is reset to its initial state, so that the same sequence of random numbers is generated each time. restore()
restores the native Math.random function.
const randomMocker = new RandomMocker();
randomMocker.mock();
Math.random(); // first call is always 0.2523451747838408
Math.random(); // second call is always 0.08812504541128874
randomMocker.mock();
Math.random(); // generator is reset, so we get 0.2523451747838408 again
randomMocker.restore();
Math.random(); // back to native Math.random
concatRegex
Combines one or more regular expressions into one.
const regex1 = /a\s/;
const regex2 = /b/;
concatRegex(regex1, regex2).source === "a\\sb";
functionRegex
Given a function name and, optionally, a list of parameters, returns a regex that can be used to match that function declaration in a code block.
let regex = functionRegex("foo", ["bar", "baz"]);
regex.test("function foo(bar, baz){}"); // true
regex.test("function foo(bar, baz, qux){}"); // false
regex.test("foo = (bar, baz) => {}"); // true
Options
- capture: boolean - If true, the regex will capture the function definition, including it's body, otherwise not. Defaults to false.
- includeBody: boolean - If true, the regex will include the function body in the match. Otherwise it will stop at the first bracket. Defaults to true.
let regEx = functionRegex("foo", ["bar", "baz"], { capture: true });
let combinedRegEx = concatRegex(/var x = "y"; /, regEx);
let match = `var x = "y";
function foo(bar, baz){}`.match(regex);
match[1]; // "function foo(bar, baz){}"
// i.e. only the function definition is captured
let regEx = functionRegex("foo", ["bar", "baz"], { includeBody: false });
let match = `function foo(bar, baz){console.log('ignored')}`.match(regex);
match[1]; // "function foo(bar, baz){"
NOTE: capture does not work properly with arrow functions. It will capture text after the function body, too.
let regEx = functionRegex("myFunc", ["arg1"], { capture: true });
let match = "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately".match(regEx);
match[1] // "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately"
CSS
Browser
CSS Code Tested
:root {
--building-color1: #aa80ff;
--building-color2: #66cc99;
--building-color3: #cc6699;
--building-color4: #538cc6;
--window-color1: #bb99ff;
--window-color2: #8cd9b3;
--window-color3: #d98cb3;
--window-color4: #8cb3d9;
}
* {
box-sizing: border-box;
}
body {
height: 100vh;
margin: 0;
overflow: hidden;
}
.background-buildings,
.foreground-buildings {
width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: space-evenly;
position: absolute;
top: 0;
}
.building-wrap {
display: flex;
flex-direction: column;
align-items: center;
}
.window-wrap {
display: flex;
align-items: center;
justify-content: space-evenly;
}
.sky {
background: radial-gradient(
closest-corner circle at 15% 15%,
#ffcf33,
#ffcf33 20%,
#ffff66 21%,
#bbeeff 100%
);
}
/* BACKGROUND BUILDINGS - "bb" stands for "background building" */
.bb1 {
width: 10%;
height: 70%;
}
.bb1a {
width: 70%;
}
.bb1b {
width: 80%;
}
.bb1c {
width: 90%;
}
.bb1d {
width: 100%;
height: 70%;
background: linear-gradient(var(--building-color1) 50%, var(--window-color1));
}
.bb1-window {
height: 10%;
background: linear-gradient(var(--building-color1), var(--window-color1));
}
.bb2 {
width: 10%;
height: 50%;
}
.bb2a {
border-bottom: 5vh solid var(--building-color2);
border-left: 5vw solid transparent;
border-right: 5vw solid transparent;
}
.bb2b {
width: 100%;
height: 100%;
background: repeating-linear-gradient(
var(--building-color2),
var(--building-color2) 6%,
var(--window-color2) 6%,
var(--window-color2) 9%
);
}
.bb3 {
width: 10%;
height: 55%;
background: repeating-linear-gradient(
90deg,
var(--building-color3),
var(--building-color3),
var(--window-color3) 15%
);
}
.bb4 {
width: 11%;
height: 58%;
}
.bb4a {
width: 3%;
height: 10%;
background-color: var(--building-color4);
}
.bb4b {
width: 80%;
height: 5%;
background-color: var(--building-color4);
}
.bb4c {
width: 100%;
height: 85%;
background-color: var(--building-color4);
}
.bb4-window {
width: 18%;
height: 90%;
background-color: var(--window-color4);
}
/* FOREGROUND BUILDINGS - "fb" stands for "foreground building" */
.fb1 {
width: 10%;
height: 60%;
}
.fb1a {
border-bottom: 7vh solid var(--building-color4);
border-left: 2vw solid transparent;
border-right: 2vw solid transparent;
}
.fb1b {
width: 60%;
height: 10%;
background-color: var(--building-color4);
}
.fb1c {
width: 100%;
height: 80%;
background: repeating-linear-gradient(
90deg,
var(--building-color4),
var(--building-color4) 10%,
transparent 10%,
transparent 15%
),
repeating-linear-gradient(
var(--building-color4),
var(--building-color4) 10%,
var(--window-color4) 10%,
var(--window-color4) 90%
);
}
.fb2 {
width: 10%;
height: 40%;
}
.fb2a {
width: 100%;
border-bottom: 10vh solid var(--building-color3);
border-left: 1vw solid transparent;
border-right: 1vw solid transparent;
}
.fb2b {
width: 100%;
height: 75%;
background-color: var(--building-color3);
}
.fb2-window {
width: 22%;
height: 100%;
background-color: var(--window-color3);
}
.fb3 {
width: 10%;
height: 35%;
}
.fb3a {
width: 80%;
height: 15%;
background-color: var(--building-color1);
}
.fb3b {
width: 100%;
height: 35%;
background-color: var(--building-color1);
}
.fb3-window {
width: 25%;
height: 80%;
background-color: var(--window-color1);
}
.fb4 {
width: 8%;
height: 45%;
position: relative;
left: 10%;
}
.fb4a {
border-top: 5vh solid transparent;
border-left: 8vw solid var(--building-color1);
}
.fb4b {
width: 100%;
height: 89%;
background-color: var(--building-color1);
display: flex;
flex-wrap: wrap;
}
.fb4-window {
width: 30%;
height: 10%;
border-radius: 50%;
background-color: var(--window-color1);
margin: 10%;
}
.fb5 {
width: 10%;
height: 33%;
position: relative;
right: 10%;
background: repeating-linear-gradient(
var(--building-color2),
var(--building-color2) 5%,
transparent 5%,
transparent 10%
),
repeating-linear-gradient(
90deg,
var(--building-color2),
var(--building-color2) 12%,
var(--window-color2) 12%,
var(--window-color2) 44%
);
}
.fb6 {
width: 9%;
height: 38%;
background: repeating-linear-gradient(
90deg,
var(--building-color3),
var(--building-color3) 10%,
transparent 10%,
transparent 30%
),
repeating-linear-gradient(
var(--building-color3),
var(--building-color3) 10%,
var(--window-color3) 10%,
var(--window-color3) 30%
);
}
@media (max-width: 1000px) {
.sky {
background: radial-gradient(
closest-corner circle at 15% 15%,
#ffcf33,
#ffcf33 20%,
#ffff66 21%,
#bbeeff 100%
);
}
}
const tester = new CSSHelp(document);
describe("getStyle", () => {
it("should return an ExtendedCSSStyleDeclartion object of length 1", () => {
expect(tester.getStyle("*")?.length).toEqual(1);
});
it("should return a non-empty ExtendedCSSStyleDeclaration object", () => {
expect(tester.getStyle(".bb1")).toBeTruthy();
});
it("should return a whitespaceless string", () => {
expect(tester.getStyle(".bb1d")?.getPropVal("background", true)).toEqual(
"linear-gradient(var(--building-color1)50%,var(--window-color1))"
);
});
});
describe("getStyleAny", () => {
it("should return an ExtendedCSSStyleDeclartion object of length 1", () => {
expect(tester.getStyleAny([".earth", ".sky"])?.length).toEqual(1);
});
it("should return null", () => {
expect(tester.getStyleAny([".sun", ".earth", ".moon"])).toBeNull();
});
});
describe("isPropertyUsed", () => {
it("should return true on existing properties", () => {
expect(tester.isPropertyUsed("height")).toBeTruthy();
});
it("should return true on existing custom properties", () => {
expect(tester.isPropertyUsed("--building-color1")).toBeTruthy();
});
});
describe("isDeclaredAfter", () => {
it("should return true if existing style is declared after another", () => {
expect(tester.getStyleRule(".bb1a")?.isDeclaredAfter(".bb1")).toBeTruthy();
});
});
describe("getPropertyValue", () => {
it("should return custom property value needing trim", () => {
expect(
tester.getStyle(":root")?.getPropertyValue("--building-color1")?.trim()
).toEqual("#aa80ff");
});
it("should return value to existing property", () => {
expect(
tester.getStyle(".bb4a")?.getPropertyValue("background-color")
).toBeTruthy();
});
it("should return property value without evaluating result", () => {
expect(
tester.getStyle(".bb4a")?.getPropertyValue("background-color")
).toEqual("var(--building-color4)");
});
});
describe("getCSSRules", () => {
it("should return a CSSRules array of length 1", () => {
expect(tester.getCSSRules("media")?.length).toEqual(1);
});
});
describe("getRuleListsWithinMedia", () => {
it("should return a CSSMediaRule array with a selectable CSSStyleRule", () => {
expect(
t
.getRuleListsWithinMedia("(max-width: 1000px)")
.find((x) => x.selectorText === ".sky")
).toBeTruthy();
});
it("should return CSSStyleDeclaration property with complex value", () => {
// NOTE: JSDOM causes value to have tabbed characters, DOM has single-line values.
expect(
t
.getRuleListsWithinMedia("(max-width: 1000px)")
.find((x) => x.selectorText === ".sky")?.style?.background
).toEqual(
`radial-gradient(
closest-corner circle at 15% 15%,
#ffcf33,
#ffcf33 20%,
#ffff66 21%,
#bbeeff 100%
)`
);
});
});
describe("selectorsFromSelector", () => {
it("should return an empty array", () => {
setupDocument();
expect(tester.selectorsFromSelector(".void")).toEqual([]);
});
it("should return an array with 9 members", () => {
setupDocument();
expect(tester.selectorsFromSelector("a")).toEqual([
"a",
"label > a",
"label a",
"form > label > a",
"form label a",
"body > form > label > a",
"body form label a",
"html > body > form > label > a",
"html body form label a",
]);
});
});
Python
Regex-based helpers
getDef
getBlock
removeComments
__pyodide.runPython
Running the code of a singular function to get the output:
AST-based helpers
In contrast to the regex-based helpers, these helpers need to be run in Python, not JavaScript
Formatting output
str
returns a string that would parse to the same AST as the node. For example:
function_str = """
def foo():
# will not be in the output
x = 1
"""
output_str = """
def foo():
x = 1"""
str(Node(function_str)) == output_str # True
The output and source string compile to the same AST, but the output is indented with 4 spaces. Comments and trailing whitespace are removed.
Finding nodes
find_
functions search the current scope and return one of the following:
- A single Node object if there can be only one match. E.g.
find_function
- A list of Node objects if there can be multiple matches. E.g.:
find_ifs
find_function
Node('def foo():\n x = "1"').find_function("foo").has_variable("x") # True
find_functions
code_str = """
class Spam(ABC):
@property
@abstractmethod
def foo(self):
return self.x
@foo.setter
@abstractmethod
def foo(self, new_x):
self.x = new_x
"""
explorer = Node(code_str)
len(explorer.find_class("Spam").find_functions("foo")) # 2
explorer.find_class("Spam").find_functions("foo")[0].is_equivalent("@property\n@abstractmethod\ndef foo(self):\n return self.x") # True
explorer.find_class("Spam").find_functions("foo")[1].is_equivalent("@foo.setter\n@abstractmethod\ndef foo(self, new_x):\n self.x = new_x") # True
find_async_function
Node('async def foo():\n await bar()').find_async_function("foo").is_equivalent("async def foo():\n await bar()") # True
find_awaits
code_str = """
async def foo(spam):
if spam:
await spam()
await bar()
await func()
"""
explorer = Node(code_str)
explorer.find_async_function("foo").find_awaits()[0].is_equivalent("await bar()") # True
explorer.find_async_function("foo").find_awaits()[1].is_equivalent("await func()") # True
explorer.find_async_function("foo").find_ifs()[0].find_awaits()[0].is_equivalent("await spam()") # True
find_variable
Node("y = 2\nx = 1").find_variable("x").is_equivalent("x = 1")
Node("a: int = 1").find_variable("a").is_equivalent("a: int = 1")
Node("self.spam = spam").find_variable("self.spam").is_equivalent("self.spam = spam")
find_aug_variable
Node("x += 1").find_aug_variable("x").is_equivalent("x += 1")
Node("x -= 1").find_aug_variable("x").is_equivalent("x -= 1")
When the variable is out of scope, find_variable
returns an None
node (i.e. Node()
or Node(None)
):
Node('def foo():\n x = "1"').find_variable("x") == Node() # True
find_body
func_str = """
def foo():
x = 1"""
Node(func_str).find_function("foo").find_body().is_equivalent("x = 1") # True
find_return
code_str = """
def foo():
if x == 1:
return False
return True
"""
Node(code_str).find_function("foo").find_return().is_equivalent("return True") # True
Node(code_str).find_function("foo").find_ifs()[0].find_return().is_equivalent("return False") # True
find_calls
code_str = """
print(1)
print(2)
foo("spam")
obj.foo("spam")
obj.bar.foo("spam")
"""
explorer = Node(code_str)
len(explorer.find_calls("print")) # 2
explorer.find_calls("print")[0].is_equivalent("print(1)")
explorer.find_calls("print")[1].is_equivalent("print(2)")
len(explorer.find_calls("foo")) # 3
explorer.find_calls("foo")[0].is_equivalent("foo('spam')")
explorer.find_calls("foo")[1].is_equivalent("obj.foo('spam')")
explorer.find_calls("foo")[2].is_equivalent("obj.bar.foo('spam')")
find_call_args
explorer = Node("print(1, 2)")
len(explorer.find_calls("print")[0].find_call_args()) # 2
explorer.find_calls("print")[0].find_call_args()[0].is_equivalent("1")
explorer.find_calls("print")[0].find_call_args()[1].is_equivalent("2")
find_class
class_str = """
class Foo:
def __init__(self):
pass
"""
Node(class_str).find_class("Foo").has_function("__init__") # True
find_ifs
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_ifs()[0].is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_ifs()[1].is_equivalent("if True:\n pass")
find_if
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_if("x == 1").is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_if("True").is_equivalent("if True:\n pass")
find_whiles
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_whiles()[0].is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_whiles()[1].is_equivalent("while False:\n pass") # True
find_while
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_while("True").is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_while("False").is_equivalent("while False:\n pass") # True
find_conditions
if_str = """
if x > 0:
x = 1
elif x < 0:
x = -1
else:
return x
"""
explorer1 = Node(if_str)
len(explorer1.find_ifs()[0].find_conditions()) # 3
explorer1.find_ifs()[0].find_conditions()[0].is_equivalent("x > 0") # True
explorer1.find_ifs()[0].find_conditions()[1].is_equivalent("x < 0") # True
explorer1.find_ifs()[0].find_conditions()[2] == Node() # True
Node("x = 1").find_conditions() # []
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_conditions()[0].is_equivalent("True") # True
explorer2.find_whiles()[0].find_conditions()[1] == Node() # True
explorer2.find_whiles()[1].find_conditions()[0].is_equivalent("False") # True
find_for_loops
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for_loops()[1].is_equivalent("for i in range(4):\n pass") # True
find_for
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for("(x, y)", "enumerate(dict)").is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for("i", "range(4)").is_equivalent("for i in range(4):\n pass") # True
find_for_vars
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_vars().is_equivalent("(x, y)") # True
explorer.find_for_loops()[1].find_for_vars().is_equivalent("i") # True
find_for_iter
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_iter().is_equivalent("enumerate(dict)") # True
explorer.find_for_loops()[1].find_for_iter().is_equivalent("range(4)") # True
find_bodies
if_str = """
if True:
x = 1
elif False:
x = 2
"""
explorer1 = Node(if_str)
explorer1.find_ifs()[0].find_bodies()[0].is_equivalent("x = 1") # True
explorer1.find_ifs()[0].find_bodies()[1].is_equivalent("x = 2") # True
while_str = """
while True:
x += 1
else:
x = 0
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_bodies()[0].is_equivalent("x += 1") # True
explorer2.find_whiles()[0].find_bodies()[1].is_equivalent("x = 0") # True
explorer2.find_whiles()[1].find_bodies()[0].is_equivalent("pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
print(x)
for i in range(4):
pass
"""
explorer3 = Node(for_str)
explorer3.find_for_loops()[0].find_bodies()[0].is_equivalent("print(x, y)") # True
explorer3.find_for_loops()[0].find_bodies()[1].is_equivalent("print(x)") # True
explorer3.find_for_loops()[1].find_bodies()[0].is_equivalent("pass") # True
find_imports
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
len(explorer.find_imports()) # 2
explorer.find_imports()[0].is_equivalent("import ast, sys")
explorer.find_imports()[1].is_equivalent("from math import factorial as f")
find_comps
Returns a list of list comprehensions, set comprehensions, dictionary comprehensions and generator expressions nodes not assigned to a variable or part of other statements.
code_str = """
[i**2 for i in lst]
(i for i in lst)
{i * j for i in spam for j in lst}
{k: v for k,v in dict}
comp = [i for i in lst]
"""
explorer = Node(code_str)
len(explorer.find_comps()) # 4
explorer.find_comps()[0].is_equivalent("[i**2 for i in lst]")
explorer.find_comps()[1].is_equivalent("(i for i in lst)")
explorer.find_comps()[2].is_equivalent("{i * j for i in spam for j in lst}")
explorer.find_comps()[3].is_equivalent("{k: v for k,v in dict}")
find_comp_iters
Returns a list of comprehension/generator expression iterables. It can be chained to find_variable
, find_return
, find_call_args()[n]
.
code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_iters()) # 1
explorer.find_variable("x").find_comp_iters()[0].is_equivalent("lst")
len(explorer.find_function("foo").find_return().find_comp_iters()) # 2
explorer.find_function("foo").find_return().find_comp_iters()[0].is_equivalent("spam")
explorer.find_function("foo").find_return().find_comp_iters()[1].is_equivalent("lst")
find_comp_targets
Returns a list of comprhension/generator expression targets (i.e. the iteration variables).
code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_targets()) # 1
explorer.find_variable("x").find_comp_targets()[0].is_equivalent("i")
len(explorer.find_function("foo").find_return().find_comp_targets()) # 2
explorer.find_function("foo").find_return().find_comp_targets()[0].is_equivalent("i")
explorer.find_function("foo").find_return().find_comp_targets()[1].is_equivalent("j")
find_comp_key
Returns the dictionary comprehension key.
code_str = """
x = {k: v for k,v in dict}
def foo(spam):
return {k: v for k in spam for v in lst}
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_key().is_equivalent("k")
explorer.find_function("foo").find_return().find_comp_key().is_equivalent("k")
find_comp_expr
Returns the expression evaluated at each iteration of comprehensions/generator expressions. It includes the if
/else
portion. In case only the if
is present, use find_comp_ifs
.
code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_expr().is_equivalent("i**2 if i else -1")
explorer.find_function("foo").find_return().find_comp_expr().is_equivalent("i * j")
find_comp_ifs
Returns a list of comprehension/generator expression if
conditions. The if
/else
construct instead is part of the expression and is found with find_comp_expr
.
code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam if i > 0 for j in lst if j != 6]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_ifs()) # 0
len(explorer.find_function("foo").find_return().find_comp_ifs()) # 2
explorer.find_function("foo").find_return().find_comp_ifs()[0].is_equivalent("i > 0")
explorer.find_function("foo").find_return().find_comp_ifs()[1].is_equivalent("j != 6")
Getting values
get_
functions return the value of the node, not the node itself.
get_variable
Node("x = 1").get_variable("x") # 1
Checking for existence
has_
functions return a boolean indicating whether the node exists.
has_variable
Node("x = 1").has_variable("x") # True
has_function
Node("def foo():\n pass").has_function("foo") # True
has_stmt
Returns a boolean indicating if the specified statement is found.
Node("name = input('hi')\nself.matrix[1][5] = 3").has_stmt("self.matrix[1][5] = 3") # True
has_args
Node("def foo(*, a, b, c=0):\n pass").find_function("foo").has_args("*, a, b, c=0") # True
has_pass
Node("def foo():\n pass").find_function("foo").has_pass() # True
Node("if x==1:\n x+=1\nelse: pass").find_ifs()[0].find_bodies()[1].has_pass() # True
has_return
Returns a boolean indicating if the function returns the specified expression/object.
code_str = """
def foo():
if x == 1:
return False
return True
"""
explorer = Node(code_str)
explorer.find_function("foo").has_return("True") # True
explorer.find_function("foo").find_ifs()[0].has_return("False") # True
has_returns
Returns a boolean indicating if the function has the specified return annotation.
Node("def foo() -> int:\n return 0").find_function("foo").has_returns("int") # True
Node("def foo() -> 'spam':\n pass").find_function("foo").has_returns("spam") # True
has_decorators
code_str = """
class A:
@property
@staticmethod
def foo():
pass
"""
Node(code_str).find_class("A").find_function("foo").has_decorators("property") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("property", "staticmethod") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("staticmethod", "property") # False, order does matter
has_call
code_str = """
print(math.sqrt(25))
if True:
spam()
"""
explorer = Node(code_str)
explorer.has_call("print(math.sqrt(25))")
explorer.find_ifs()[0].find_bodies()[0].has_call("spam()")
has_import
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
explorer.has_import("import ast, sys")
explorer.has_import("from math import factorial as f")
has_class
code_str = """
class spam:
pass
"""
Node(code_str).has_class("spam")
Misc
is_equivalent
This is a somewhat loose check. The AST of the target string and the AST of the node do not have to be identical, but the code must be equivalent.
Node("x = 1").is_equivalent("x = 1") # True
Node("\nx = 1").is_equivalent("x = 1") # True
Node("x = 1").is_equivalent("x = 2") # False
is_empty
This is syntactic sugar for == Node()
.
Node().is_empty() # True
Node("x = 1").find_variable("x").is_empty() # False
get the nth statement
stmts = """
if True:
pass
x = 1
"""
Node(stmts).get_nth_statement(1).is_equivalent("x = 1") # True
value_is_call
This allows you to check if the return value of function call is assigned to a variable.
explorer = Node("def foo():\n x = bar()")
explorer.find_function("foo").find_variable("x").value_is_call("bar") # True
is_integer
Node("x = 1").find_variable("x").is_integer() # True
Node("x = '1'").find_variable("x").is_integer() # False
inherits_from
Node("class C(A, B):\n pass").find_class("C").inherits_from("A") # True
Node("class C(A, B):\n pass").find_class("C").inherits_from("A", "B") # True
is_ordered
Returs a boolean indicating if the statements passed as arguments are found in the same order in the tree (statements can be non-consecutive)
code_str = """
x = 1
if x:
print("x is:")
y = 0
print(x)
return y
x = 0
"""
if_str = """
if x:
print("x is:")
y = 0
print(x)
return y
"""
explorer = Node(code_str)
explorer.is_ordered("x=1", "x=0") # True
explorer.is_ordered("x=1", if_str, "x=0") # True
explorer.find_ifs()[0].is_ordered("print('x is:')", "print(x)", "return y") # True
explorer.is_ordered("x=0", "x=1") # False
explorer.find_ifs()[0].is_ordered("print(x)", "print('x is:')") # False
Notes on Python
- Python does not allow newline characters between keywords and their arguments. E.g:
def
add(x, y):
return x + y
- Python does allow newline characters between function arguments. E.g:
def add(
x,
y
):
return x + y
Common Tools
Some of the examples assume global access to the above tools. Usage of these helpers is denoted by code referencing __helpers
or __pyodide
.
Testing in General
Tips
For any test of any language, the result should be preferred to be tested as opposed to the implementation. For example, if testing a function that adds two numbers, assume the body of the function is unknown, and test the result of running the function with various inputs.
This cannot be done in all cases. E.g:
- Testing Rust in the browser with no way to compile/run the code
- The code does not run, because it is incomplete or contains syntax errors
freeCodeCamp
2023/12/03: The below might be outdated, by the time you read
The freeCodeCamp editor/test-runner uses \r\n
as line endings. This needs to be taken into account when directly writing tests on the code strings.
Rust
Regex
Testing a struct:
const code = `
fn main() {
let _ = StructName {
one_field: 1,
field_name: [1, 2]
};
}
struct StructName {
one_field: usize,
field_name: [usize; 2]
}
`;
{
const struct = code.match(/struct StructName[^\{]*?{([^\}]*)}/s)?.[1];
console.assert(struct.match(/field_name\s*:/));
console.log(struct);
}
Remove two or more space/tab characters to reduce the number of \s
checks:
const code = `
fn main() {
let _ = StructName {
one_field: "example",
field_name: [1, 2]
};
}
struct StructName<'a> {
one_field : &'a str ,
field_name: [usize; 2]
}
`;
{
// Keywords **require** one or more spaces. So, replace instances of 2+ spaces
// with one space, to turn `/struct\s+StringName/` into `/struct StringName/`
const reducedCode = code.replaceAll(/[ \t]{2,}/g, ' ');
const struct = reducedCode.match(/struct StructName[^\{]*?{([^\}]*)}/s)?.[1];
console.assert(struct.match(/field_name\s*:/));
console.log(struct);
}
Contributing
Library API
Documentation
Tooling
Development
mdbook serve