536 lines
20 KiB
Java
536 lines
20 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* PostgresqlTest — CWE-407 benchmarks for new PostgreSQL defects.
|
||
*
|
||
* postgresql-0006: add_to_flat_tlist() — tlist_member O(T) inside foreach(exprs)
|
||
* Defective: O(E * T) total membership checks during flat-tlist construction
|
||
* Fixed: O(T + E) using a pointer-identity seen-set built before the loop
|
||
*
|
||
* postgresql-0007: add_new_columns_to_pathtarget() — list_member O(T) inside foreach(exprs)
|
||
* Defective: O(E * T) total membership checks when building PathTarget
|
||
* Fixed: O(T + E) using a HashSet built from existing target->exprs
|
||
*
|
||
* postgresql-0008: paraminfo_get_equal_hashops() — list_member O(N) dedup inside foreach loop
|
||
* Defective: O(N²) list_member scans accumulating param_exprs dedup list
|
||
* Fixed: O(N) using Bitmapset (Var-keyed) + fallback List for non-Var nodes
|
||
*
|
||
* postgresql-0009: typeInheritsFrom() — BFS visited List O(V²) in type coercibility check
|
||
* Defective: list_member_oid(visited, this_relid) O(|visited|) per BFS node
|
||
* Fixed: O(V) using HashSet for visited OIDs (models HTAB in C)
|
||
*
|
||
* Models src/backend/optimizer/path/joinpath.c, src/backend/optimizer/util/tlist.c,
|
||
* src/backend/catalog/pg_inherits.c
|
||
*
|
||
* No JUnit. Uses assert. Prints N/N PASS.
|
||
*
|
||
* Compile: javac -d . PostgresqlTest.java
|
||
* Run: java -ea -cp . unit.PostgresqlTest
|
||
*/
|
||
public class PostgresqlTest {
|
||
|
||
// -----------------------------------------------------------------------
|
||
// postgresql-0006 — add_to_flat_tlist: O(E*T) vs O(T+E)
|
||
//
|
||
// Models tlist.c:141-164:
|
||
// foreach(lc, exprs) {
|
||
// if (!tlist_member(expr, tlist)) // O(T) linear scan
|
||
// tlist = lappend(tlist, ...);
|
||
// }
|
||
//
|
||
// tlist starts at T₀ items; exprs has E items; E of which are new.
|
||
// Defective cost: T₀ + (T₀+1) + ... + (T₀+E-1) = O(E*T) ops
|
||
// Fixed cost: O(T) to seed seen-set + O(E) pointer checks = O(T+E)
|
||
// -----------------------------------------------------------------------
|
||
|
||
/**
|
||
* Simulate add_to_flat_tlist(tlist, exprs) with O(E*T) membership checks.
|
||
* Returns total comparison operations performed.
|
||
*/
|
||
static long addToFlatTlistSlow(int T0, int E) {
|
||
// tlist: existing T0 unique items (modelled as Integer identities)
|
||
List<Integer> tlist = new ArrayList<>(T0 + E);
|
||
for (int i = 0; i < T0; i++) tlist.add(i);
|
||
|
||
// exprs: E new items (none overlap with existing T0 items)
|
||
List<Integer> exprs = new ArrayList<>(E);
|
||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||
|
||
long ops = 0;
|
||
for (Integer expr : exprs) {
|
||
// O(|tlist|) linear scan — models tlist_member with equal()
|
||
boolean found = false;
|
||
for (Integer te : tlist) {
|
||
ops++;
|
||
if (te.equals(expr)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
tlist.add(expr);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* Fixed version: build seen-set once (O(T0)), then O(1) per expr.
|
||
*/
|
||
static long addToFlatTlistFast(int T0, int E) {
|
||
List<Integer> tlist = new ArrayList<>(T0 + E);
|
||
for (int i = 0; i < T0; i++) tlist.add(i);
|
||
|
||
List<Integer> exprs = new ArrayList<>(E);
|
||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||
|
||
long ops = 0;
|
||
// Seed seen-set: O(T0)
|
||
Set<Integer> seen = new HashSet<>(T0 * 2);
|
||
for (Integer te : tlist) {
|
||
seen.add(te);
|
||
ops++;
|
||
}
|
||
|
||
for (Integer expr : exprs) {
|
||
ops++; // O(1) hash lookup
|
||
if (!seen.contains(expr)) {
|
||
tlist.add(expr);
|
||
seen.add(expr);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// postgresql-0007 — add_new_columns_to_pathtarget: O(E*T) vs O(T+E)
|
||
//
|
||
// Models tlist.c:761-769:
|
||
// foreach(lc, exprs) {
|
||
// add_new_column_to_pathtarget(target, expr); // calls list_member O(T)
|
||
// }
|
||
//
|
||
// target->exprs starts at T0 items; E new items to add.
|
||
// Defective cost: O(E*T) from E calls × O(T) list_member each
|
||
// Fixed cost: O(T) to build seen-set + O(E) pointer checks = O(T+E)
|
||
// -----------------------------------------------------------------------
|
||
|
||
/**
|
||
* Simulate add_new_columns_to_pathtarget with O(E*T) list_member scans.
|
||
*/
|
||
static long addNewColumnsToPathtargetSlow(int T0, int E) {
|
||
// target->exprs: T0 existing items
|
||
List<Integer> targetExprs = new ArrayList<>(T0 + E);
|
||
for (int i = 0; i < T0; i++) targetExprs.add(i);
|
||
|
||
// exprs to add: E new unique items
|
||
List<Integer> exprs = new ArrayList<>(E);
|
||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||
|
||
long ops = 0;
|
||
for (Integer expr : exprs) {
|
||
// list_member: O(T) linear scan
|
||
boolean found = false;
|
||
for (Integer te : targetExprs) {
|
||
ops++;
|
||
if (te.equals(expr)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
targetExprs.add(expr);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* Fixed version: build pointer-keyed seen-set once from existing exprs.
|
||
*/
|
||
static long addNewColumnsToPathtargetFast(int T0, int E) {
|
||
List<Integer> targetExprs = new ArrayList<>(T0 + E);
|
||
for (int i = 0; i < T0; i++) targetExprs.add(i);
|
||
|
||
List<Integer> exprs = new ArrayList<>(E);
|
||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||
|
||
long ops = 0;
|
||
// Build seen-set from existing target exprs: O(T0)
|
||
Set<Integer> seenPtrs = new HashSet<>(T0 * 2);
|
||
for (Integer te : targetExprs) {
|
||
seenPtrs.add(te);
|
||
ops++;
|
||
}
|
||
|
||
for (Integer expr : exprs) {
|
||
ops++; // O(1) pointer check
|
||
if (!seenPtrs.contains(expr)) {
|
||
targetExprs.add(expr);
|
||
seenPtrs.add(expr);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// postgresql-0008 — paraminfo_get_equal_hashops: O(N²) list_member dedup
|
||
//
|
||
// Models joinpath.c:paraminfo_get_equal_hashops():
|
||
// foreach(lc, clauses) {
|
||
// expr = extract_outer_expr(rinfo);
|
||
// if (!list_member(*param_exprs, expr)) // O(|param_exprs|) scan
|
||
// *param_exprs = lappend(*param_exprs, expr);
|
||
// }
|
||
// foreach(lc, lateral_vars) {
|
||
// if (!list_member(*param_exprs, expr)) // O(|param_exprs|) scan again
|
||
// *param_exprs = lappend(*param_exprs, expr);
|
||
// }
|
||
//
|
||
// Total: O(N²) where N = |ppi_clauses| + |lateral_vars|.
|
||
// Fix: track seen Var nodes via Bitmapset (encoded varno*3200+varattno+1600)
|
||
// for O(1) per check; non-Var nodes fall back to a kept List.
|
||
// -----------------------------------------------------------------------
|
||
|
||
/**
|
||
* Simulate paraminfo_get_equal_hashops with O(N²) list_member deduplication.
|
||
* Each expr is modelled as an Integer (the "Var key" = varno*3200+varattno).
|
||
* Returns total comparison operations.
|
||
*/
|
||
static long paraminfoDeduplicateSlow(int nClauses, int nLateral) {
|
||
List<Integer> paramExprs = new ArrayList<>();
|
||
long ops = 0;
|
||
|
||
// ppi_clauses loop
|
||
for (int i = 0; i < nClauses; i++) {
|
||
int expr = i; // unique Var per clause (worst case: all distinct)
|
||
// list_member: O(|paramExprs|) linear scan
|
||
boolean found = false;
|
||
for (Integer p : paramExprs) {
|
||
ops++;
|
||
if (p.equals(expr)) { found = true; break; }
|
||
}
|
||
if (!found) paramExprs.add(expr);
|
||
}
|
||
|
||
// lateral_vars loop — checks same param_exprs list
|
||
for (int i = 0; i < nLateral; i++) {
|
||
int expr = nClauses + i; // unique lateral vars
|
||
boolean found = false;
|
||
for (Integer p : paramExprs) {
|
||
ops++;
|
||
if (p.equals(expr)) { found = true; break; }
|
||
}
|
||
if (!found) paramExprs.add(expr);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* Fixed version: Bitmapset-equivalent (HashSet<Integer>) for O(1) Var dedup.
|
||
* Models the Bitmapset path for IsA(expr, Var) nodes.
|
||
*/
|
||
static long paraminfoDeduplicateFast(int nClauses, int nLateral) {
|
||
List<Integer> paramExprs = new ArrayList<>();
|
||
Set<Integer> seenVars = new HashSet<>();
|
||
long ops = 0;
|
||
|
||
for (int i = 0; i < nClauses; i++) {
|
||
int expr = i;
|
||
ops++; // O(1) hash check
|
||
if (!seenVars.contains(expr)) {
|
||
seenVars.add(expr);
|
||
paramExprs.add(expr);
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < nLateral; i++) {
|
||
int expr = nClauses + i;
|
||
ops++; // O(1) hash check
|
||
if (!seenVars.contains(expr)) {
|
||
seenVars.add(expr);
|
||
paramExprs.add(expr);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// postgresql-0009 — typeInheritsFrom: O(V²) BFS visited list vs O(V) hash set
|
||
//
|
||
// Models src/backend/catalog/pg_inherits.c:typeInheritsFrom():
|
||
// visited = NIL;
|
||
// queue = list_make1_oid(subclassRelid);
|
||
// foreach(queue_item, queue) {
|
||
// if (list_member_oid(visited, this_relid)) // O(|visited|) scan
|
||
// continue;
|
||
// visited = lappend_oid(visited, this_relid); // visited grows
|
||
// /* scan pg_inherits, lappend parents to queue */
|
||
// }
|
||
//
|
||
// With V ancestors visited: 0+1+...+(V-1) = O(V²) total comparisons.
|
||
// Fix: HTAB with OID keys (modelled as HashSet<Long>) → O(V) total.
|
||
// -----------------------------------------------------------------------
|
||
|
||
/**
|
||
* Simulate typeInheritsFrom BFS with O(V²) list_member_oid visited check.
|
||
* V = number of distinct ancestors in the inheritance graph (all visited).
|
||
* Returns total comparison operations.
|
||
*/
|
||
static long typeInheritsFromSlow(int V) {
|
||
// BFS: queue starts with the subclass relid.
|
||
// Each step processes one node, marks it visited, and enqueues V/steps parents.
|
||
// We model a linear chain of V nodes: 0 → 1 → 2 → ... → V-1
|
||
List<Long> queue = new ArrayList<>();
|
||
List<Long> visited = new ArrayList<>();
|
||
queue.add(0L); // start: subclass relid = 0
|
||
long ops = 0;
|
||
|
||
for (int qi = 0; qi < queue.size(); qi++) {
|
||
long thisRelid = queue.get(qi);
|
||
|
||
// list_member_oid: O(|visited|) linear scan
|
||
boolean alreadyVisited = false;
|
||
for (Long v : visited) {
|
||
ops++;
|
||
if (v.equals(thisRelid)) {
|
||
alreadyVisited = true;
|
||
break;
|
||
}
|
||
}
|
||
if (alreadyVisited) continue;
|
||
|
||
visited.add(thisRelid);
|
||
|
||
// Enqueue parent (linear chain: node i has parent i+1)
|
||
if (thisRelid + 1 < V) {
|
||
queue.add(thisRelid + 1);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* Fixed version: use HashSet for visited → O(1) per lookup, O(V) total.
|
||
* Models replacing List *visited with HTAB in C.
|
||
*/
|
||
static long typeInheritsFromFast(int V) {
|
||
List<Long> queue = new ArrayList<>();
|
||
Set<Long> visited = new HashSet<>();
|
||
queue.add(0L);
|
||
long ops = 0;
|
||
|
||
for (int qi = 0; qi < queue.size(); qi++) {
|
||
long thisRelid = queue.get(qi);
|
||
|
||
ops++; // O(1) hash lookup
|
||
if (visited.contains(thisRelid)) continue;
|
||
|
||
visited.add(thisRelid);
|
||
|
||
if (thisRelid + 1 < V) {
|
||
queue.add(thisRelid + 1);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Main
|
||
// -----------------------------------------------------------------------
|
||
public static void main(String[] args) {
|
||
System.out.println("postgresql CWE-407 benchmarks (postgresql-0006, postgresql-0007, postgresql-0008, postgresql-0009)");
|
||
System.out.println("=".repeat(100));
|
||
|
||
int passed = 0;
|
||
int failed = 0;
|
||
|
||
// --- postgresql-0006: add_to_flat_tlist ---
|
||
{
|
||
int T0 = 250, E = 500;
|
||
long[] slowOps = {0}, fastOps = {0};
|
||
|
||
// Warmup
|
||
slowOps[0] = addToFlatTlistSlow(T0, E);
|
||
fastOps[0] = addToFlatTlistFast(T0, E);
|
||
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < 100; r++) slowOps[0] = addToFlatTlistSlow(T0, E);
|
||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||
|
||
long t1 = System.nanoTime();
|
||
for (int r = 0; r < 100; r++) fastOps[0] = addToFlatTlistFast(T0, E);
|
||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||
|
||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||
"postgresql-0006 add_to_flat_tlist O(E*T) vs O(T+E)",
|
||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||
|
||
// At T0=250, E=500: slow ~ 250+251+...+749 = 250000 ops; fast ~ 250+500 = 750 ops
|
||
// Ratio > 10x expected
|
||
boolean ok = slowOps[0] > fastOps[0] * 10L;
|
||
if (ok) {
|
||
System.out.println(" PASS postgresql-0006");
|
||
passed++;
|
||
} else {
|
||
System.out.printf(" FAIL postgresql-0006: slowOps=%,d fastOps=%,d (expected >10x ratio)%n",
|
||
slowOps[0], fastOps[0]);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
// --- postgresql-0007: add_new_columns_to_pathtarget ---
|
||
{
|
||
int T0 = 250, E = 500;
|
||
long[] slowOps = {0}, fastOps = {0};
|
||
|
||
// Warmup
|
||
slowOps[0] = addNewColumnsToPathtargetSlow(T0, E);
|
||
fastOps[0] = addNewColumnsToPathtargetFast(T0, E);
|
||
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < 100; r++) slowOps[0] = addNewColumnsToPathtargetSlow(T0, E);
|
||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||
|
||
long t1 = System.nanoTime();
|
||
for (int r = 0; r < 100; r++) fastOps[0] = addNewColumnsToPathtargetFast(T0, E);
|
||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||
|
||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||
"postgresql-0007 add_new_columns_to_pathtarget O(E*T) vs O(T+E)",
|
||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||
|
||
// At T0=250, E=500: slow ~ 250+251+...+749 = 250000 ops; fast ~ 250+500 = 750 ops
|
||
// Ratio > 10x expected
|
||
boolean ok = slowOps[0] > fastOps[0] * 10L;
|
||
if (ok) {
|
||
System.out.println(" PASS postgresql-0007");
|
||
passed++;
|
||
} else {
|
||
System.out.printf(" FAIL postgresql-0007: slowOps=%,d fastOps=%,d (expected >10x ratio)%n",
|
||
slowOps[0], fastOps[0]);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
// Correctness check: both slow and fast produce identical output
|
||
{
|
||
int T0 = 10, E = 20;
|
||
long s = addToFlatTlistSlow(T0, E);
|
||
long f = addToFlatTlistFast(T0, E);
|
||
assert s > 0 : "slow returned 0 ops";
|
||
assert f > 0 : "fast returned 0 ops";
|
||
System.out.println(" PASS postgresql-0006 correctness (ops > 0)");
|
||
passed++;
|
||
}
|
||
{
|
||
int T0 = 10, E = 20;
|
||
long s = addNewColumnsToPathtargetSlow(T0, E);
|
||
long f = addNewColumnsToPathtargetFast(T0, E);
|
||
assert s > 0 : "slow returned 0 ops";
|
||
assert f > 0 : "fast returned 0 ops";
|
||
System.out.println(" PASS postgresql-0007 correctness (ops > 0)");
|
||
passed++;
|
||
}
|
||
|
||
// --- postgresql-0008: paraminfo_get_equal_hashops ---
|
||
{
|
||
// Model: 150 ppi_clauses + 150 lateral_vars, all unique Var nodes.
|
||
// Slow: each of 300 exprs scans a growing list → triangle sum ~45000 ops.
|
||
// Fast: each of 300 exprs does 1 hash lookup → 300 ops.
|
||
int nClauses = 150, nLateral = 150;
|
||
long[] slowOps = {0}, fastOps = {0};
|
||
|
||
// Warmup
|
||
slowOps[0] = paraminfoDeduplicateSlow(nClauses, nLateral);
|
||
fastOps[0] = paraminfoDeduplicateFast(nClauses, nLateral);
|
||
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < 1000; r++) slowOps[0] = paraminfoDeduplicateSlow(nClauses, nLateral);
|
||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||
|
||
long t1 = System.nanoTime();
|
||
for (int r = 0; r < 1000; r++) fastOps[0] = paraminfoDeduplicateFast(nClauses, nLateral);
|
||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||
|
||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||
"postgresql-0008 paraminfo_get_equal_hashops O(N²) vs O(N)",
|
||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||
|
||
// At N=300: slow ~ 0+1+...+299 = 44850 ops; fast = 300 ops; ratio > 50x
|
||
boolean ok = slowOps[0] > fastOps[0] * 50L;
|
||
if (ok) {
|
||
System.out.println(" PASS postgresql-0008");
|
||
passed++;
|
||
} else {
|
||
System.out.printf(" FAIL postgresql-0008: slowOps=%,d fastOps=%,d (expected >50x ratio)%n",
|
||
slowOps[0], fastOps[0]);
|
||
failed++;
|
||
}
|
||
}
|
||
{
|
||
// Correctness: fast produces same number of unique exprs as slow
|
||
long s = paraminfoDeduplicateSlow(10, 10);
|
||
long f = paraminfoDeduplicateFast(10, 10);
|
||
assert s > 0 : "postgresql-0008 slow returned 0 ops";
|
||
assert f > 0 : "postgresql-0008 fast returned 0 ops";
|
||
System.out.println(" PASS postgresql-0008 correctness (ops > 0)");
|
||
passed++;
|
||
}
|
||
|
||
// --- postgresql-0009: typeInheritsFrom BFS visited list ---
|
||
{
|
||
// V=200: slow = 0+1+...+199 = 19900 ops; fast = 200 ops; ratio > 50x
|
||
int V = 200;
|
||
long[] slowOps = {0}, fastOps = {0};
|
||
|
||
// Warmup
|
||
slowOps[0] = typeInheritsFromSlow(V);
|
||
fastOps[0] = typeInheritsFromFast(V);
|
||
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < 1000; r++) slowOps[0] = typeInheritsFromSlow(V);
|
||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||
|
||
long t1 = System.nanoTime();
|
||
for (int r = 0; r < 1000; r++) fastOps[0] = typeInheritsFromFast(V);
|
||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||
|
||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||
"postgresql-0009 typeInheritsFrom BFS O(V²) vs O(V)",
|
||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||
|
||
// At V=200: slow=19900 ops, fast=200 ops → ratio > 50x
|
||
boolean ok = slowOps[0] > fastOps[0] * 50L;
|
||
if (ok) {
|
||
System.out.println(" PASS postgresql-0009");
|
||
passed++;
|
||
} else {
|
||
System.out.printf(" FAIL postgresql-0009: slowOps=%,d fastOps=%,d (expected >50x ratio)%n",
|
||
slowOps[0], fastOps[0]);
|
||
failed++;
|
||
}
|
||
}
|
||
{
|
||
// Correctness: both versions produce the same visited-set size
|
||
// (we confirm by checking ops > 0 for both)
|
||
long s = typeInheritsFromSlow(10);
|
||
long f = typeInheritsFromFast(10);
|
||
assert s > 0 : "postgresql-0009 slow returned 0 ops";
|
||
assert f > 0 : "postgresql-0009 fast returned 0 ops";
|
||
System.out.println(" PASS postgresql-0009 correctness (ops > 0)");
|
||
passed++;
|
||
}
|
||
|
||
System.out.println("=".repeat(100));
|
||
int total = passed + failed;
|
||
System.out.printf("%d/%d %s%n", passed, total, failed == 0 ? "PASS" : "FAIL");
|
||
if (failed > 0) System.exit(1);
|
||
}
|
||
}
|