Initial commit: OTA chunk transfer protocol implementation
- Protocol definition with 6-byte header (chunk_idx: 2B, payload: N B, crc32: 4B) - Node.js server with chunk-based HTTP delivery - C client library with resumable download support - CRC32 implementation (IEEE 802.3) - Test suites for both server and CRC32 - 1024B default chunk size (0.59% overhead)
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @file test_server.js
|
||||
* @brief Server Test Suite
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ChunkServer = require('../server/server');
|
||||
|
||||
const PORT = 18080;
|
||||
const TEST_DIR = path.join(__dirname, 'test_files');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(cond, msg) {
|
||||
if (cond) {
|
||||
console.log(`[PASS] ${msg}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`[FAIL] ${msg}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP GET helper
|
||||
*/
|
||||
function httpRequest(options, expectedStatus = 200) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(options, (res) => {
|
||||
let data = Buffer.alloc(0);
|
||||
res.on('data', chunk => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: data
|
||||
});
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test files
|
||||
*/
|
||||
function setupTestFiles() {
|
||||
if (!fs.existsSync(TEST_DIR)) {
|
||||
fs.mkdirSync(TEST_DIR);
|
||||
}
|
||||
|
||||
// Create test file: 3072 bytes (3 chunks of 1024)
|
||||
const testData = Buffer.alloc(3072);
|
||||
for (let i = 0; i < 3072; i++) {
|
||||
testData[i] = i % 256;
|
||||
}
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'test.bin'), testData);
|
||||
|
||||
// Create small file: 100 bytes
|
||||
const smallData = Buffer.alloc(100);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
smallData[i] = 0x42;
|
||||
}
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'small.bin'), smallData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup test files
|
||||
*/
|
||||
function cleanupTestFiles() {
|
||||
try {
|
||||
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test suite
|
||||
*/
|
||||
async function runTests() {
|
||||
console.log("========================================");
|
||||
console.log(" Chunk Server Test Suite");
|
||||
console.log("========================================\n");
|
||||
|
||||
// Setup
|
||||
setupTestFiles();
|
||||
|
||||
const server = new ChunkServer({
|
||||
port: PORT,
|
||||
rootPath: TEST_DIR,
|
||||
chunkSize: 1024
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log("\n--- Server Tests ---");
|
||||
|
||||
try {
|
||||
// Test 1: Get first chunk
|
||||
const res1 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=test.bin',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res1.statusCode === 200, "First chunk returns 200");
|
||||
assert(res1.headers['content-type'] === 'application/octet-stream', "Content-Type is octet-stream");
|
||||
assert(res1.headers['x-file-size'] === '3072', "X-File-Size header present");
|
||||
assert(res1.headers['x-total-chunks'] === '3', "X-Total-Chunks header present");
|
||||
assert(res1.body.length === 2 + 1024 + 4, "First chunk size correct (header + payload + crc)");
|
||||
|
||||
// Verify chunk index in payload
|
||||
assert(res1.body.readUInt16LE(0) === 0, "Chunk index is 0");
|
||||
|
||||
// Test 2: Get second chunk
|
||||
const res2 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=test.bin&chunk=1',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res2.statusCode === 206, "Second chunk returns 206");
|
||||
assert(res2.body.readUInt16LE(0) === 1, "Chunk index is 1");
|
||||
assert(res2.body.length === 2 + 1024 + 4, "Second chunk size correct");
|
||||
|
||||
// Test 3: Get last chunk (smaller)
|
||||
const res3 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=test.bin&chunk=2',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res3.statusCode === 206, "Last chunk returns 206");
|
||||
assert(res3.body.readUInt16LE(0) === 2, "Chunk index is 2");
|
||||
assert(res3.body.length === 2 + 1024 + 4, "Last chunk size correct");
|
||||
|
||||
// Test 4: Invalid chunk index
|
||||
const res4 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=test.bin&chunk=10',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res4.statusCode === 416, "Invalid chunk returns 416");
|
||||
|
||||
// Test 5: File not found
|
||||
const res5 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=nonexistent.bin',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res5.statusCode === 404, "Missing file returns 404");
|
||||
|
||||
// Test 6: Missing file parameter
|
||||
const res6 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res6.statusCode === 400, "Missing file param returns 400");
|
||||
|
||||
// Test 7: Small file (single chunk)
|
||||
const res7 = await httpRequest({
|
||||
hostname: 'localhost',
|
||||
port: PORT,
|
||||
path: '/ota?file=small.bin',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
assert(res7.statusCode === 200, "Small file returns 200");
|
||||
assert(res7.headers['x-total-chunks'] === '1', "Small file has 1 chunk");
|
||||
assert(res7.body.length === 2 + 100 + 4, "Small file chunk size correct");
|
||||
|
||||
// Test 8: Verify CRC32
|
||||
const payload = res1.body.slice(2, 2 + 1024);
|
||||
const receivedCrc = res1.body.readUInt32LE(2 + 1024);
|
||||
const calculatedCrc = ChunkServer.crc32(payload);
|
||||
console.log(`Received CRC: ${receivedCrc.toString(16)}`);
|
||||
console.log(`Calculated CRC: ${calculatedCrc.toString(16)}`);
|
||||
console.log(`Calculated >>>0: ${(calculatedCrc >>> 0).toString(16)}`);
|
||||
assert(receivedCrc === (calculatedCrc >>> 0), "CRC32 verification passed");
|
||||
|
||||
} catch (err) {
|
||||
console.error("Test error:", err);
|
||||
failed++;
|
||||
} finally {
|
||||
await server.stop();
|
||||
cleanupTestFiles();
|
||||
}
|
||||
|
||||
console.log("\n========================================");
|
||||
console.log(` Results: ${passed} passed, ${failed} failed`);
|
||||
console.log("========================================");
|
||||
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
runTests().catch(err => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user