const axios = require('axios'); const YAML = require('yaml'); const fs = require('node:fs'); const path_ = require('node:path'); const url = require('node:url'); const https = require('node:https'); const Assert = require('./Assert'); const log_error = require('./log_error'); module.exports = class TestSDK { constructor (conf, context) { this.conf = conf; this.context = context; this.cwd = `/${conf.username}`; this.httpsAgent = new https.Agent({ rejectUnauthorized: false }) const url_origin = new url.URL(conf.url).origin; this.headers_ = { 'Origin': url_origin, 'Authorization': `Bearer ${conf.token}` }; this.installAPIMethodShorthands_(); this.assert = new Assert(); this.sdks = {}; this.results = []; this.failCount = 0; this.caseCount = 0; this.nameStack = []; this.packageResults = []; this.benchmarkResults = []; } async get_sdk (name) { return await this.sdks[name].create(); } // === test related methods === async runTestPackage (testDefinition) { this.nameStack.push(testDefinition.name); this.packageResults.push({ name: testDefinition.name, failCount: 0, caseCount: 0, }); const imported = {}; for ( const key of Object.keys(testDefinition.import ?? {}) ) { imported[key] = this.sdks[key]; } await testDefinition.do(this, imported); this.nameStack.pop(); } async runBenchmark (benchDefinition) { const strid = '' + '\x1B[35;1m[bench]\x1B[0m' + this.nameStack.join(` \x1B[36;1m->\x1B[0m `); process.stdout.write(strid + ' ... \n'); this.nameStack.push(benchDefinition.name); let results; this.benchmarkResults.push(results = { name: benchDefinition.name, start: Date.now(), }); try { await benchDefinition.do(this); } catch (e) { results.error = e; } finally { results.end = Date.now(); const dur = results.end - results.start; process.stdout.write(`...\x1B[32;1m[${dur}]\x1B[0m\n`); } } recordResult (result) { const pkg = this.packageResults[this.packageResults.length - 1]; this.caseCount++; pkg.caseCount++; if ( ! result.success ) { this.failCount++; pkg.failCount++; } this.results.push(result); } async case (id, fn) { this.nameStack.push(id); const tabs = Array(this.nameStack.length - 2).fill(' ').join(''); const strid = tabs + this.nameStack.join(` \x1B[36;1m->\x1B[0m `); process.stdout.write(strid + ' ... \n'); try { await fn(this.context); } catch (e) { process.stdout.write(`${tabs}...\x1B[31;1m[FAIL]\x1B[0m\n`); this.recordResult({ strid, e, success: false, }); log_error(e); return; } finally { this.nameStack.pop(); } process.stdout.write(`${tabs}...\x1B[32;1m[PASS]\x1B[0m\n`); this.recordResult({ strid, success: true }); } quirk (msg) { console.log(`\x1B[33;1mignoring known quirk: ${msg}\x1B[0m`); } // === information display methods === printTestResults () { console.log(`\n\x1B[33;1m=== Test Results ===\x1B[0m`); let tbl = {}; for ( const pkg of this.packageResults ) { tbl[pkg.name] = { passed: pkg.caseCount - pkg.failCount, failed: pkg.failCount, total: pkg.caseCount, } } console.table(tbl); process.stdout.write(`\x1B[36;1m${this.caseCount} tests were run\x1B[0m - `); if ( this.failCount > 0 ) { console.log(`\x1B[31;1m✖ ${this.failCount} tests failed!\x1B[0m`); } else { console.log(`\x1B[32;1m✔ All tests passed!\x1B[0m`) } } printBenchmarkResults () { console.log(`\n\x1B[33;1m=== Benchmark Results ===\x1B[0m`); let tbl = {}; for ( const bench of this.benchmarkResults ) { tbl[bench.name] = { time: bench.end - bench.start, error: bench.error ? bench.error.message : '', } } console.table(tbl); } // === path related methods === cd (path) { this.cwd = path_.posix.join(this.cwd, path); } resolve (path) { if ( path.startsWith('$') ) return path; if ( path.startsWith('/') ) return path; return path_.posix.join(this.cwd, path); } // === API calls === installAPIMethodShorthands_ () { const p = this.resolve.bind(this); this.read = async path => { const res = await this.get('read', { path: p(path) }); return res.data; } this.mkdir = async (path, opts) => { const res = await this.post('mkdir', { path: p(path), ...(opts ?? {}) }); return res.data; }; this.write = async (path, bin, params) => { path = p(path); params = params ?? {}; let mime = 'text/plain'; if ( params.hasOwnProperty('mime') ) { mime = params.mime; delete params.mime; } let name = path_.posix.basename(path); path = path_.posix.dirname(path); params.path = path; const res = await this.upload('write', name, mime, bin, params); return res.data; } this.stat = async (path, params) => { path = p(path); const res = await this.post('stat', { ...params, path }); return res.data; } this.statu = async (uid, params) => { const res = await this.post('stat', { ...params, uid }); return res.data; } this.readdir = async (path, params) => { path = p(path); const res = await this.post('readdir', { ...params, path }) return res.data; } this.delete = async (path, params) => { path = p(path); const res = await this.post('delete', { ...params, paths: [path] }); return res.data; } this.move = async (src, dst, params = {}) => { src = p(src); dst = p(dst); const destination = path_.dirname(dst); const source = src; const new_name = path_.basename(dst); console.log('move', { destination, source, new_name }); const res = await this.post('move', { ...params, destination, source, new_name, }); return res.data; } } getURL (...path) { const apiURL = new url.URL(this.conf.url); apiURL.pathname = path_.posix.join( apiURL.pathname, ...path ); return apiURL.href; }; // === HTTP methods === get (ep, params) { return axios.request({ httpsAgent: this.httpsAgent, method: 'get', url: this.getURL(ep), params, headers: { ...this.headers_ } }); } post (ep, params) { return axios.request({ httpsAgent: this.httpsAgent, method: 'post', url: this.getURL(ep), data: params, headers: { ...this.headers_, 'Content-Type': 'application/json', } }) } upload (ep, name, mime, bin, params) { const adapt_file = (bin, mime) => { if ( typeof bin === 'string' ) { return new Blob([bin], { type: mime }); } return bin; }; const fd = new FormData(); for ( const k in params ) fd.append(k, params[k]); const blob = adapt_file(bin, mime); fd.append('size', blob.size); fd.append('file', adapt_file(bin, mime), name) return axios.request({ httpsAgent: this.httpsAgent, method: 'post', url: this.getURL(ep), data: fd, headers: { ...this.headers_, 'Content-Type': 'multipart/form-data' }, }); } async batch (ep, ops, bins) { const adapt_file = (bin, mime) => { if ( typeof bin === 'string' ) { return new Blob([bin], { type: mime }); } return bin; }; const fd = new FormData(); fd.append('original_client_socket_id', ''); fd.append('socket_id', ''); fd.append('operation_id', ''); let fileI = 0; for ( let i=0 ; i < ops.length ; i++ ) { const op = ops[i]; fd.append('operation', JSON.stringify(op)); } const files = []; for ( let i=0 ; i < ops.length ; i++ ) { const op = ops[i]; if ( op.op === 'mkdir' ) continue; if ( op.op === 'mktree' ) continue; let mime = op.mime ?? 'text/plain'; const file = adapt_file(bins[fileI++], mime); fd.append('fileinfo', JSON.stringify({ size: file.size, name: op.name, mime, })); files.push({ op, file, }) delete op.name; } for ( const file of files ) { const { op, file: blob } = file; fd.append('file', blob, op.name); } const res = await axios.request({ httpsAgent: this.httpsAgent, method: 'post', url: this.getURL(ep), data: fd, headers: { ...this.headers_, 'Content-Type': 'multipart/form-data' }, }); return res.data.results; } batch_json (ep, ops, bins) { return axios.request({ httpsAgent: this.httpsAgent, method: 'post', url: this.getURL(ep), data: ops, headers: { ...this.headers_, 'Content-Type': 'application/json', }, }); } }