-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstress-test.py
More file actions
executable file
·469 lines (410 loc) · 17.2 KB
/
Copy pathstress-test.py
File metadata and controls
executable file
·469 lines (410 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
#!/usr/bin/env python3
"""
Runs CppMode's full parser/pipeline stress-test suite.
Works no matter where you put this file (project root, scripts/,
wherever) -- it finds the real CppMode checkout by walking up looking
for mode.properties, rather than assuming its own location is the root.
Before running the suite, it writes a small set of known-good fixed/new
files into place (the corrected Scrollbar.pde example, and 5 new-user
edge-case fixtures: blank template, only-setup, only-draw, empty file,
comment-only file). This means a single plain run does everything --
nothing else needs to be created or copy-pasted by hand. Pass --no-write
to skip this step if you'd rather manage those files yourself.
Then it compiles the standalone tools/cpp-parser/ project and runs it
against every real example sketch under examples/, in four layers:
1. ParserCorpusSweep -- parse-only check (fastest, no g++ needed)
2. RealCorpusStressTest -- full pipeline + g++ against a hand-built,
deliberately narrow Processing API stub.
A non-zero fail count here is EXPECTED and
not a sign of a problem -- see Layer 3.
3. RealHeaderStressTest -- full pipeline + g++ against the REAL
Processing.h/Processing_api.h headers
(needs libglfw3-dev + libglew-dev installed;
auto-skipped with a clear message if not
found, not a hard failure). This is the
layer whose fail count should be at or near
zero.
4. RealHeaderStressTest again, against new-user edge cases (the blank
template, partial sketches) instead of the real example corpus.
Same real-headers requirement as Layer 3.
Usage:
stress-test.py
stress-test.py --quick (skip layers 3/4, no GLFW3/GLEW needed)
stress-test.py --no-write (skip writing the known-good fixtures)
"""
import os
import shutil
import subprocess
import sys
# ──────────────────────────────────────────────────────────────────────
# Root detection: walk up from this script's own location looking for
# mode.properties (CppMode's real top-level marker file), rather than
# assuming this script's own directory IS the project root. Works
# whether this file lives at the project root or in scripts/.
# ──────────────────────────────────────────────────────────────────────
def find_cppmode_root():
here = os.path.dirname(os.path.abspath(__file__))
candidate = here
for _ in range(5):
if os.path.isfile(os.path.join(candidate, "mode.properties")):
return candidate
parent = os.path.dirname(candidate)
if parent == candidate:
break
candidate = parent
print(f"ERROR: could not find CppMode's mode.properties by walking up from {here}.")
print("This script needs to live somewhere inside a real CppMode checkout")
print("(either at the project root, or in scripts/, examples/, tools/, etc.)")
sys.exit(1)
CPPMODE_DIR = find_cppmode_root()
CPP_PARSER_DIR = os.path.join(CPPMODE_DIR, "tools", "cpp-parser")
EXAMPLES_DIR = os.path.join(CPPMODE_DIR, "examples")
SRC_DIR = os.path.join(CPPMODE_DIR, "src")
OUT_DIR = os.path.join(CPP_PARSER_DIR, "out")
CORPUS_DIR = "/tmp/cppmode_stress_corpus"
# ──────────────────────────────────────────────────────────────────────
# Known-good fixed/new files, written into place by default. Pass
# --no-write to skip this step.
#
# NOTE: src/java/Parser.java and src/java/CodeGen.java are NOT written
# here -- they're large, hand-maintained files delivered separately.
# If Layer 3 shows Pie_Chart.pde / Game_Of_Life.pde / Characters_Strings.pde
# / Scrollbar.pde failing (or CRASHing), those two files are out of date;
# this script can't fix that for you, only flag it.
# ──────────────────────────────────────────────────────────────────────
FILES_TO_WRITE = {
"examples/Topics/gui/Scrollbar/Scrollbar.pde": '''/**
* Scrollbar.
*
* Move the scrollbars left and right to change the positions of the images.
*/
//True if a mouse button was pressed while no other button was.
boolean firstMousePress = false;
HScrollbar* hs1, * hs2; // Two scrollbars
PImage* img1 = nullptr;
PImage* img2 = nullptr; // Two images to load
void setup() {
size(640, 360);
noStroke();
hs1 = new HScrollbar(0, height/2-8, width, 16, 16);
hs2 = new HScrollbar(0, height/2+8, width, 16, 16);
// Load images
img1 = loadImage("seedTop.jpg");
img2 = loadImage("seedBottom.jpg");
}
void draw() {
background(255);
// Get the position of the img1 scrollbar
// and convert to a value to display the img1 image
float img1Pos = hs1->getPos()-width/2;
fill(255);
image(img1, width/2-img1->width/2 + img1Pos*1.5, 0);
// Get the position of the img2 scrollbar
// and convert to a value to display the img2 image
float img2Pos = hs2->getPos()-width/2;
fill(255);
image(img2, width/2-img2->width/2 + img2Pos*1.5, height/2);
hs1->update();
hs2->update();
hs1->display();
hs2->display();
stroke(0);
line(0, height/2, width, height/2);
//After it has been used in the sketch, set it back to false
if (firstMousePress) {
firstMousePress = false;
}
}
void mousePressed() {
if (!firstMousePress) {
firstMousePress = true;
}
}
class HScrollbar {
int swidth, sheight; // width and height of bar
float xpos, ypos; // x and y position of bar
float spos, newspos; // x position of slider
float sposMin, sposMax; // max and min values of slider
int loose; // how loose/heavy
boolean over; // is the mouse over the slider?
boolean locked;
float ratio;
HScrollbar (float xp, float yp, int sw, int sh, int l) {
swidth = sw;
sheight = sh;
int widthtoheight = sw - sh;
ratio = (float)sw / (float)widthtoheight;
xpos = xp;
ypos = yp-sheight/2;
spos = xpos + swidth/2 - sheight/2;
newspos = spos;
sposMin = xpos;
sposMax = xpos + swidth - sheight;
loose = l;
}
void update() {
if (overEvent()) {
over = true;
} else {
over = false;
}
if (firstMousePress && over) {
locked = true;
}
if (!_mousePressed) {
locked = false;
}
if (locked) {
newspos = constrain(mouseX-sheight/2, sposMin, sposMax);
}
if (abs(newspos - spos) > 1) {
spos = spos + (newspos-spos)/loose;
}
}
float constrain(float val, float minv, float maxv) {
return min(max(val, minv), maxv);
}
boolean overEvent() {
if (mouseX > xpos && mouseX < xpos+swidth &&
mouseY > ypos && mouseY < ypos+sheight) {
return true;
} else {
return false;
}
}
void display() {
noStroke();
fill(204);
rect(xpos, ypos, swidth, sheight);
if (over || locked) {
fill(0, 0, 0);
} else {
fill(102, 102, 102);
}
rect(spos, ypos, sheight, sheight);
}
float getPos() {
// Convert spos to be values between
// 0 and the total width of the scrollbar
return spos * ratio;
}
}
''',
"tools/cpp-parser/fixtures/new-user-edge-cases/blank_template.pde": '''// Full C++ STL is available without any #include.
// See https://processing-cpp.github.io/libraries
void setup() {
// put your setup code here, to run once:
}
void draw() {
// put your main code here, to run repeatedly:
}
''',
"tools/cpp-parser/fixtures/new-user-edge-cases/only_setup.pde": '''void setup() {
size(400, 400);
}
''',
"tools/cpp-parser/fixtures/new-user-edge-cases/only_draw.pde": '''void draw() {
background(0);
}
''',
"tools/cpp-parser/fixtures/new-user-edge-cases/completely_empty.pde": "",
"tools/cpp-parser/fixtures/new-user-edge-cases/just_a_comment.pde": "// nothing here yet\n",
}
def write_known_good_files():
print("=" * 70)
print("STEP 0: writing known-good fixed/new files into place")
print("=" * 70)
for rel_path, content in FILES_TO_WRITE.items():
full_path = os.path.join(CPPMODE_DIR, rel_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(content)
print(f" wrote {rel_path} ({len(content)} bytes)")
print()
# ──────────────────────────────────────────────────────────────────────
# Compile + run the 4-layer suite.
# ──────────────────────────────────────────────────────────────────────
def run(cmd, cwd=None, check=True):
print(f"$ {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=cwd)
if check and result.returncode != 0:
print(f"FAILED (exit code {result.returncode}): {' '.join(cmd)}")
sys.exit(result.returncode)
return result.returncode
def find_java_sources(*subdirs):
files = []
for sub in subdirs:
base = os.path.join(CPP_PARSER_DIR, sub)
for root, _, names in os.walk(base):
for name in names:
if name.endswith(".java"):
files.append(os.path.join(root, name))
return files
def compile_parser_project():
if not os.path.isdir(os.path.join(CPP_PARSER_DIR, "src", "main")):
print(f"ERROR: {CPP_PARSER_DIR}/src/main not found.")
print("Is tools/cpp-parser/ actually present and complete?")
sys.exit(1)
sources = find_java_sources("src/main", "src/test")
if not sources:
print(f"ERROR: no .java files found under {CPP_PARSER_DIR}/src/")
sys.exit(1)
print(f"Compiling {len(sources)} Java source files...")
os.makedirs(OUT_DIR, exist_ok=True)
run(["javac", "-d", OUT_DIR] + sources)
def gather_real_corpus():
if os.path.isdir(CORPUS_DIR):
shutil.rmtree(CORPUS_DIR)
os.makedirs(CORPUS_DIR)
if not os.path.isdir(EXAMPLES_DIR):
print(f"ERROR: examples directory not found at {EXAMPLES_DIR}")
sys.exit(1)
count = 0
for root, _, names in os.walk(EXAMPLES_DIR):
for name in names:
if name.endswith(".pde"):
shutil.copy(os.path.join(root, name), CORPUS_DIR)
count += 1
if count == 0:
print(f"ERROR: no .pde files found under {EXAMPLES_DIR}")
sys.exit(1)
print(f"Gathered {count} real example sketches into {CORPUS_DIR}")
return count
def has_real_headers():
"""True if libglfw3-dev/libglew-dev are installed -- checked via
pkg-config first, falling back to a direct header existence check."""
for pkg in ("glfw3", "glew"):
result = subprocess.run(["pkg-config", "--exists", pkg],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if result.returncode != 0:
candidates = {
"glfw3": ["/usr/include/GLFW/glfw3.h", "/usr/local/include/GLFW/glfw3.h"],
"glew": ["/usr/include/GL/glew.h", "/usr/local/include/GL/glew.h"],
}
if not any(os.path.exists(p) for p in candidates[pkg]):
return False
return True
def run_parser_sweep_layer():
print()
print("=" * 70)
print("LAYER 1: ParserCorpusSweep (parse-only, no g++ needed)")
print("=" * 70)
run(["java", "-cp", OUT_DIR, "cppmode.parser.ParserCorpusSweep", CORPUS_DIR],
cwd=CPP_PARSER_DIR, check=False)
def run_stub_pipeline_layer(quick):
print()
print("=" * 70)
print("LAYER 2: RealCorpusStressTest (full pipeline + g++, narrow stub)")
print("=" * 70)
if quick:
print("Skipped (--quick).")
return
if has_real_headers():
print("Skipped: real headers (libglfw3-dev/libglew-dev) are available,")
print("so Layer 3 already gives the real, trustworthy signal -- this")
print("layer's deliberately tiny, hand-written API stub only ever")
print("covers a small slice of the real Processing API, so its g++")
print("failures (RGB, PI, PShape, etc. \"not declared\") are EXPECTED")
print("noise, not real problems, and just distract from Layer 3's")
print("answer. Pass --force-layer-2 to run it anyway.")
if "--force-layer-2" not in sys.argv:
return
run(["java", "-cp", OUT_DIR, "cppmode.parser.RealCorpusStressTest", CORPUS_DIR],
cwd=CPP_PARSER_DIR, check=False)
def run_real_header_layer(quick):
print()
print("=" * 70)
print("LAYER 3: RealHeaderStressTest (full pipeline + g++, REAL Processing.h)")
print("=" * 70)
if quick:
print("Skipped (--quick).")
return
if not has_real_headers():
print("Skipped: libglfw3-dev and/or libglew-dev not found.")
print("Install them to run this layer, e.g. on Debian/Ubuntu:")
print(" sudo apt-get install libglfw3-dev libglew-dev")
print("On Arch:")
print(" sudo pacman -S glfw glew")
return
fixtures_stub = os.path.join(CPP_PARSER_DIR, "fixtures", "extracted_real_api_stub.h")
if not os.path.exists(fixtures_stub):
print(f"Skipped: {fixtures_stub} not found.")
print("This file is expected alongside the other tools/cpp-parser/fixtures/.")
return
# cwd=CPP_PARSER_DIR is required: RealCorpusStressTest/RealHeaderStressTest
# both read "fixtures/extracted_real_api_stub.h" as a path relative to
# the JVM's working directory, not relative to this script's location.
run(["java", "-cp", OUT_DIR, "cppmode.parser.RealHeaderStressTest", CORPUS_DIR, SRC_DIR],
cwd=CPP_PARSER_DIR, check=False)
def run_new_user_edge_cases_layer(quick):
print()
print("=" * 70)
print("LAYER 4: new-user edge cases (blank template, partial sketches)")
print("=" * 70)
edge_cases_dir = os.path.join(CPP_PARSER_DIR, "fixtures", "new-user-edge-cases")
if not os.path.isdir(edge_cases_dir):
print(f"Skipped: {edge_cases_dir} not found.")
return
if quick:
print("Skipped (--quick) -- this layer also needs real headers.")
return
if not has_real_headers():
print("Skipped: libglfw3-dev and/or libglew-dev not found (same requirement as Layer 3).")
return
run(["java", "-cp", OUT_DIR, "cppmode.parser.RealHeaderStressTest", edge_cases_dir, SRC_DIR],
cwd=CPP_PARSER_DIR, check=False)
def run_stress_cases_layer():
"""
Layer 5: the ever-growing adversarial stress-cases/ folder.
Pure parser + codegen, no g++ and no real headers needed, so this
always runs regardless of --quick or GLFW3/GLEW availability -- it's
cheap and should never be skipped.
Each .cpp file under tools/cpp-parser/stress-cases/ declares its own
expectation via a "// EXPECT: PASS" or "// EXPECT: FAIL" header
comment. PASS files that fail are real regressions (nonzero exit).
FAIL files that start passing are reported but not treated as
failures -- just a sign that file's header may be worth updating.
To add a new adversarial case: drop a new .cpp file in
tools/cpp-parser/stress-cases/ with an EXPECT header and a short
explanation of what it's probing. No code changes needed -- it's
picked up automatically next run.
"""
print()
print("=" * 70)
print("LAYER 5: stress-cases (ever-growing adversarial parser fixtures)")
print("=" * 70)
stress_cases_dir = os.path.join(CPP_PARSER_DIR, "stress-cases")
if not os.path.isdir(stress_cases_dir):
print(f"Skipped: {stress_cases_dir} not found.")
return
run(["java", "-cp", OUT_DIR, "cppmode.parser.StressCasesRunner", stress_cases_dir],
cwd=CPP_PARSER_DIR, check=False)
def main():
quick = "--quick" in sys.argv
no_write = "--no-write" in sys.argv
print(f"CppMode root: {CPPMODE_DIR}")
print(f"cpp-parser at: {CPP_PARSER_DIR}")
print(f"examples at: {EXAMPLES_DIR}")
print()
if not no_write:
write_known_good_files()
compile_parser_project()
gather_real_corpus()
run_parser_sweep_layer()
run_stub_pipeline_layer(quick)
run_real_header_layer(quick)
run_new_user_edge_cases_layer(quick)
run_stress_cases_layer()
print()
print("=" * 70)
print("Stress test run complete.")
print("=" * 70)
print("Read each layer's own summary above for pass/fail counts.")
print("'PIPELINE CRASHES' is the number that matters most -- that's a")
print("real Java exception in the pass logic, not g++ noise.")
print("Layer 3/4's g++ fail count is the real signal -- it should be at or")
print("near zero against the actual Processing.h.")
print("Everything else is explained in")
print("tools/cpp-parser/DECISION_two_parser_implementations.md.")
if __name__ == "__main__":
main()