Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules

# Build artifacts
*.o
*.exe
Expand Down Expand Up @@ -131,3 +133,5 @@ Thumbs.db
.github/copilot/
CLAUDE.md
.aider*
web/barracuda.wasm
web/barracuda.js
35 changes: 33 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CFLAGS = -std=c99 -Wall -Wextra -pedantic -O2 \
-Wdouble-promotion -Wswitch-enum -Wwrite-strings \
-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE $(CF_PROT) \
$(GCC_ONLY) \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime -Iruntime
LDFLAGS = -pie
LIBS = -lm
# Linux/ELF only: -Wl,-z,relro,-z,now -Wl,-z,noexecstack
Expand Down Expand Up @@ -92,7 +92,38 @@ src/runtime/%.o: src/runtime/%.c
runtime/%.o: runtime/%.c
$(CC) $(TCFLAGS) -c $< -o $@

WASM_OUT_DIR = web
WASM_TARGET = $(WASM_OUT_DIR)/barracuda.js

wasm: $(SOURCES)
@mkdir -p $(WASM_OUT_DIR)
emcc $(SOURCES) -O3 \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime -Iruntime \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_RUNTIME_METHODS='["FS","callMain"]' \
-s INVOKE_RUN=0 \
-s EXPORT_ES6=0 \
-o $(WASM_TARGET)
@echo "WASM compilation successful. Output in $(WASM_OUT_DIR)/"

wasm_test: wasm
./tests/test_wasm_build.sh
node ./tests/test_wasm_run.js
node ./tests/test_worker.js
node ./tests/test_app.js

wasm_test_e2e: wasm
@if ! node -e "require('puppeteer')" > /dev/null 2>&1; then \
echo "Ensure puppeteer is installed via 'npm install puppeteer --no-save' before running this target."; \
exit 1; \
fi
node ./tests/test_e2e_web.js

wasm_serve: wasm
@echo "Starting web server on http://localhost:8000"
@python3 -m http.server 8000 --directory web

clean:
rm -f $(OBJECTS) $(TARGET) $(TARGET).exe trunner trunner.exe $(TOBJS) src/runtime/*.o runtime/*.o

.PHONY: all clean test
.PHONY: all clean test wasm wasm_test wasm_serve
24 changes: 20 additions & 4 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,28 @@ static int run_bir_backends(bir_module_t *bir, const backend_cfg_t *cfg)
}
}
if (arc == BC_OK) {
if (cfg->mode_amdgpu_bin)
if (cfg->mode_amdgpu_bin) {
amdgpu_emit_elf(amd,
cfg->output_file ? cfg->output_file : "a.hsaco");
else
amdgpu_emit_asm(amd, stdout);
} else {
} else {
FILE *out = stdout;
if (cfg->output_file) {
out = fopen(cfg->output_file, "w");
if (!out) {
fprintf(stderr, "error: could not open output file %s\n", cfg->output_file);
arc = BC_ERR_IO;
}
}
if (arc == BC_OK) {
amdgpu_emit_asm(amd, out);
if (out != stdout) {
fclose(out);
}
}
}
}

if (arc != BC_OK) {
if (arc != BC_ERR_VERIFY)
fprintf(stderr, "error: AMDGPU compilation failed\n");
rc = arc;
Expand Down
142 changes: 142 additions & 0 deletions tests/test_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @file test_app.js
* @description Lightweight DOM mock test for app.js to ensure event bindings and state updates work.
*/

const fs = require('fs');
const path = require('path');

// Basic DOM Mock
class DOMNode {
constructor(tagName) {
this.tagName = tagName;
this.children = [];
this.attributes = {};
this.events = {};
this.value = '';
this.textContent = '';
this.disabled = false;
this.scrollTop = 0;
this.scrollHeight = 100;
}

appendChild(child) {
this.children.push(child);
}

addEventListener(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}

dispatchEvent(eventObj) {
const cbs = this.events[eventObj.type] || [];
for (const cb of cbs) cb(eventObj);
}
}

const mockDocument = {
elements: {},
getElementById(id) {
if (!this.elements[id]) {
this.elements[id] = new DOMNode('div');
}
return this.elements[id];
},
createElement(tagName) {
return new DOMNode(tagName);
}
};

const mockWindow = new DOMNode('window');

// Mock Monaco
const mockMonaco = {
editor: {
create: () => ({
getValue: () => "mock code",
setValue: () => {},
getModel: () => ({})
}),
setModelLanguage: () => {}
}
};

global.document = mockDocument;
global.window = mockWindow;
global.monaco = mockMonaco;

// Create a global require function for the eval context
const mockRequire = (deps, cb) => {
if (cb) cb();
};
mockRequire.config = () => {};
global.require = mockRequire;

// Mock Worker
class MockWorker {
constructor(script) {
this.script = script;
}
postMessage(msg) {
// Simulate immediate response
if (msg.command === 'compile') {
setTimeout(() => {
this.onmessage({ data: { type: 'compile_result', exitCode: 0, output: 'mock output' } });
}, 10);
}
}
}
global.Worker = MockWorker;

// Load app.js
const appJsPath = path.resolve(__dirname, '../web/app.js');
const appJsCode = fs.readFileSync(appJsPath, 'utf8');

// Evaluate app.js
let testCode = appJsCode.replace(/require/g, 'mockRequire');
testCode = testCode.replace(/let /g, 'var ').replace(/const /g, 'var ');
eval(testCode);

// Trigger DOMContentLoaded
mockWindow.dispatchEvent({ type: 'DOMContentLoaded' });

// Test initial state
const btn = mockDocument.getElementById('compile-btn');
if (btn.textContent !== 'Loading...') {
console.error("Test failed: Button should be in Loading state initially.");
process.exit(1);
}

// Simulate Worker Ready
eval("compilerWorker.onmessage({ data: { type: 'ready' } })");

if (btn.disabled !== false || btn.textContent !== 'Compile') {
console.error("Test failed: Button should be enabled and say 'Compile' after worker ready.");
process.exit(1);
}

// Trigger compile
btn.dispatchEvent({ type: 'click' });

if (btn.disabled !== true || btn.textContent !== 'Compiling...') {
console.error("Test failed: Button should be disabled and say 'Compiling...' during compile.");
process.exit(1);
}

// Wait for mock compile result
setTimeout(() => {
if (btn.disabled !== false || btn.textContent !== 'Compile') {
console.error("Test failed: Button should be reset after compile.");
process.exit(1);
}

const outputView = mockDocument.getElementById('output-view');
if (outputView.value !== 'mock output') {
console.error("Test failed: Output view did not receive mock output.");
process.exit(1);
}

console.log("All app.js mock tests passed.");
process.exit(0);
}, 50);
148 changes: 148 additions & 0 deletions tests/test_e2e_web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* @file test_e2e_web.js
* @description End-to-End integration test for the BarraCUDA Web Compiler.
* Spins up a local HTTP server and uses Puppeteer to verify the UI,
* Web Worker, and compilation pipeline end-to-end.
*/

const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = 3000;
const WEB_DIR = path.resolve(__dirname, '../web');

// 1. Setup a simple static HTTP server
const server = http.createServer((req, res) => {
let filePath = path.join(WEB_DIR, req.url === '/' ? 'index.html' : req.url);

const extname = String(path.extname(filePath)).toLowerCase();
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.wasm': 'application/wasm'
};

const contentType = mimeTypes[extname] || 'application/octet-stream';

fs.readFile(filePath, (error, content) => {
if (error) {
if(error.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found', 'utf-8');
} else {
res.writeHead(500);
res.end('Server Error: '+error.code+' ..\n');
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});

/**
* Runs the E2E tests using Puppeteer.
*/
async function runE2ETests() {
let puppeteer;
try {
puppeteer = require('puppeteer');
} catch (e) {
console.warn("Puppeteer is not installed. Skipping full browser E2E test.");
console.log("To run full E2E tests, install puppeteer: npm install puppeteer");

// At least verify files exist to satisfy the asset check
const assets = ['index.html', 'style.css', 'app.js', 'wasm-worker.js', 'barracuda.wasm', 'barracuda.js'];
for (const asset of assets) {
if (!fs.existsSync(path.join(WEB_DIR, asset))) {
console.error("Asset " + asset + " is missing.");
process.exit(1);
}
}
console.log("All web assets are present.");
process.exit(0);
return;
}

console.log("Starting Puppeteer E2E tests...");
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();

const errors = [];
page.on('pageerror', err => errors.push(err.toString()));
page.on('requestfailed', request => {
errors.push("Request failed: " + request.url() + " (" + request.failure().errorText + ")");
});

await page.goto("http://localhost:" + PORT + "/", { waitUntil: 'networkidle0' });

// Verify UI Elements exist
const btnText = await page.$eval('#compile-btn', el => el.textContent);
if (!btnText.includes('Compile') && !btnText.includes('Loading')) {
throw new Error("Compile button not found or incorrect text.");
}

// Wait for worker to be ready
await page.waitForFunction(() => {
const btn = document.getElementById('compile-btn');
return btn && btn.disabled === false && btn.textContent === 'Compile';
}, { timeout: 10000 });

console.log("Worker initialized in browser.");

const examples = ['vector_add', 'matmul'];
const targets = ['--nvidia-ptx', '--amdgpu', '--tensix', '--cpu'];

for (const example of examples) {
for (const target of targets) {
console.log("Testing combination: Example=" + example + ", Target=" + target + " ...");

// Select Example
await page.select('#example-select', example);
await new Promise(r => setTimeout(r, 500)); // wait for editor update

// Select Target
await page.select('#target-select', target);

// Trigger Compile
await page.click('#compile-btn');

// Wait for compilation to finish (button re-enables)
await page.waitForFunction(() => {
const btn = document.getElementById('compile-btn');
return btn && btn.disabled === false;
}, { timeout: 15000 });

const consoleOut = await page.$eval('#console-view', el => el.value);
const outputText = await page.$eval('#output-view', el => el.value);

if (consoleOut.includes('failed with exit code')) {
console.warn("Warning: Compilation failed for " + example + " with " + target + ". Console:", consoleOut);
} else if (outputText.includes('could not read output file')) {
throw new Error("Output file reading failed for " + example + " with " + target);
} else {
console.log("Combination " + example + " + " + target + " compiled successfully.");
}
}
}

if (errors.length > 0) {
console.error("Browser errors encountered:", errors);
throw new Error("Browser E2E encountered errors.");
}

await browser.close();
console.log("All E2E tests passed successfully!");
process.exit(0);
}

// Start Server and Run Tests
server.listen(PORT, () => {
console.log("Test HTTP server running at http://localhost:" + PORT);
runE2ETests().catch(err => {
console.error("E2E Test Failed:", err);
process.exit(1);
});
});
Loading