TestSDK.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. const axios = require('axios');
  2. const YAML = require('yaml');
  3. const fs = require('node:fs');
  4. const path_ = require('node:path');
  5. const url = require('node:url');
  6. const https = require('node:https');
  7. const Assert = require('./Assert');
  8. const log_error = require('./log_error');
  9. module.exports = class TestSDK {
  10. constructor (conf) {
  11. this.conf = conf;
  12. this.cwd = `/${conf.username}`;
  13. this.httpsAgent = new https.Agent({
  14. rejectUnauthorized: false
  15. })
  16. const url_origin = new url.URL(conf.url).origin;
  17. this.headers_ = {
  18. 'Origin': url_origin,
  19. 'Authorization': `Bearer ${conf.token}`
  20. };
  21. this.installAPIMethodShorthands_();
  22. this.assert = new Assert();
  23. this.sdks = {};
  24. this.results = [];
  25. this.failCount = 0;
  26. this.caseCount = 0;
  27. this.nameStack = [];
  28. this.packageResults = [];
  29. this.benchmarkResults = [];
  30. }
  31. async get_sdk (name) {
  32. return await this.sdks[name].create();
  33. }
  34. // === test related methods ===
  35. async runTestPackage (testDefinition) {
  36. this.nameStack.push(testDefinition.name);
  37. this.packageResults.push({
  38. name: testDefinition.name,
  39. failCount: 0,
  40. caseCount: 0,
  41. });
  42. const imported = {};
  43. for ( const key of Object.keys(testDefinition.import ?? {}) ) {
  44. imported[key] = this.sdks[key];
  45. }
  46. await testDefinition.do(this, imported);
  47. this.nameStack.pop();
  48. }
  49. async runBenchmark (benchDefinition) {
  50. const strid = '' +
  51. '\x1B[35;1m[bench]\x1B[0m' +
  52. this.nameStack.join(` \x1B[36;1m->\x1B[0m `);
  53. process.stdout.write(strid + ' ... \n');
  54. this.nameStack.push(benchDefinition.name);
  55. let results;
  56. this.benchmarkResults.push(results = {
  57. name: benchDefinition.name,
  58. start: Date.now(),
  59. });
  60. try {
  61. await benchDefinition.do(this);
  62. } catch (e) {
  63. results.error = e;
  64. } finally {
  65. results.end = Date.now();
  66. const dur = results.end - results.start;
  67. process.stdout.write(`...\x1B[32;1m[${dur}]\x1B[0m\n`);
  68. }
  69. }
  70. recordResult (result) {
  71. const pkg = this.packageResults[this.packageResults.length - 1];
  72. this.caseCount++;
  73. pkg.caseCount++;
  74. if ( ! result.success ) {
  75. this.failCount++;
  76. pkg.failCount++;
  77. }
  78. this.results.push(result);
  79. }
  80. async case (id, fn) {
  81. this.nameStack.push(id);
  82. const tabs = Array(this.nameStack.length - 2).fill(' ').join('');
  83. const strid = tabs + this.nameStack.join(` \x1B[36;1m->\x1B[0m `);
  84. process.stdout.write(strid + ' ... \n');
  85. try {
  86. await fn();
  87. } catch (e) {
  88. process.stdout.write(`${tabs}...\x1B[31;1m[FAIL]\x1B[0m\n`);
  89. this.recordResult({
  90. strid,
  91. e,
  92. success: false,
  93. });
  94. log_error(e);
  95. return;
  96. } finally {
  97. this.nameStack.pop();
  98. }
  99. process.stdout.write(`${tabs}...\x1B[32;1m[PASS]\x1B[0m\n`);
  100. this.recordResult({
  101. strid,
  102. success: true
  103. });
  104. }
  105. quirk (msg) {
  106. console.log(`\x1B[33;1mignoring known quirk: ${msg}\x1B[0m`);
  107. }
  108. // === information display methods ===
  109. printTestResults () {
  110. console.log(`\n\x1B[33;1m=== Test Results ===\x1B[0m`);
  111. let tbl = {};
  112. for ( const pkg of this.packageResults ) {
  113. tbl[pkg.name] = {
  114. passed: pkg.caseCount - pkg.failCount,
  115. failed: pkg.failCount,
  116. total: pkg.caseCount,
  117. }
  118. }
  119. console.table(tbl);
  120. process.stdout.write(`\x1B[36;1m${this.caseCount} tests were run\x1B[0m - `);
  121. if ( this.failCount > 0 ) {
  122. console.log(`\x1B[31;1m✖ ${this.failCount} tests failed!\x1B[0m`);
  123. } else {
  124. console.log(`\x1B[32;1m✔ All tests passed!\x1B[0m`)
  125. }
  126. }
  127. printBenchmarkResults () {
  128. console.log(`\n\x1B[33;1m=== Benchmark Results ===\x1B[0m`);
  129. let tbl = {};
  130. for ( const bench of this.benchmarkResults ) {
  131. tbl[bench.name] = {
  132. time: bench.end - bench.start,
  133. error: bench.error ? bench.error.message : '',
  134. }
  135. }
  136. console.table(tbl);
  137. }
  138. // === path related methods ===
  139. cd (path) {
  140. this.cwd = path_.posix.join(this.cwd, path);
  141. }
  142. resolve (path) {
  143. if ( path.startsWith('$') ) return path;
  144. if ( path.startsWith('/') ) return path;
  145. return path_.posix.join(this.cwd, path);
  146. }
  147. // === API calls ===
  148. installAPIMethodShorthands_ () {
  149. const p = this.resolve.bind(this);
  150. this.read = async path => {
  151. const res = await this.get('read', { path: p(path) });
  152. return res.data;
  153. }
  154. this.mkdir = async (path, opts) => {
  155. const res = await this.post('mkdir', {
  156. path: p(path),
  157. ...(opts ?? {})
  158. });
  159. return res.data;
  160. };
  161. this.write = async (path, bin, params) => {
  162. path = p(path);
  163. params = params ?? {};
  164. let mime = 'text/plain';
  165. if ( params.hasOwnProperty('mime') ) {
  166. mime = params.mime;
  167. delete params.mime;
  168. }
  169. let name = path_.posix.basename(path);
  170. path = path_.posix.dirname(path);
  171. params.path = path;
  172. const res = await this.upload('write', name, mime, bin, params);
  173. return res.data;
  174. }
  175. this.stat = async (path, params) => {
  176. path = p(path);
  177. const res = await this.post('stat', { ...params, path });
  178. return res.data;
  179. }
  180. this.statu = async (uid, params) => {
  181. const res = await this.post('stat', { ...params, uid });
  182. return res.data;
  183. }
  184. this.readdir = async (path, params) => {
  185. path = p(path);
  186. const res = await this.post('readdir', {
  187. ...params,
  188. path
  189. })
  190. return res.data;
  191. }
  192. this.delete = async (path, params) => {
  193. path = p(path);
  194. const res = await this.post('delete', {
  195. ...params,
  196. paths: [path]
  197. });
  198. return res.data;
  199. }
  200. this.move = async (src, dst, params = {}) => {
  201. src = p(src);
  202. dst = p(dst);
  203. const destination = path_.dirname(dst);
  204. const source = src;
  205. const new_name = path_.basename(dst);
  206. console.log('move', { destination, source, new_name });
  207. const res = await this.post('move', {
  208. ...params,
  209. destination,
  210. source,
  211. new_name,
  212. });
  213. return res.data;
  214. }
  215. }
  216. getURL (...path) {
  217. const apiURL = new url.URL(this.conf.url);
  218. apiURL.pathname = path_.posix.join(
  219. apiURL.pathname,
  220. ...path
  221. );
  222. return apiURL.href;
  223. };
  224. // === HTTP methods ===
  225. get (ep, params) {
  226. return axios.request({
  227. httpsAgent: this.httpsAgent,
  228. method: 'get',
  229. url: this.getURL(ep),
  230. params,
  231. headers: {
  232. ...this.headers_
  233. }
  234. });
  235. }
  236. post (ep, params) {
  237. return axios.request({
  238. httpsAgent: this.httpsAgent,
  239. method: 'post',
  240. url: this.getURL(ep),
  241. data: params,
  242. headers: {
  243. ...this.headers_,
  244. 'Content-Type': 'application/json',
  245. }
  246. })
  247. }
  248. upload (ep, name, mime, bin, params) {
  249. const adapt_file = (bin, mime) => {
  250. if ( typeof bin === 'string' ) {
  251. return new Blob([bin], { type: mime });
  252. }
  253. return bin;
  254. };
  255. const fd = new FormData();
  256. for ( const k in params ) fd.append(k, params[k]);
  257. const blob = adapt_file(bin, mime);
  258. fd.append('size', blob.size);
  259. fd.append('file', adapt_file(bin, mime), name)
  260. return axios.request({
  261. httpsAgent: this.httpsAgent,
  262. method: 'post',
  263. url: this.getURL(ep),
  264. data: fd,
  265. headers: {
  266. ...this.headers_,
  267. 'Content-Type': 'multipart/form-data'
  268. },
  269. });
  270. }
  271. async batch (ep, ops, bins) {
  272. const adapt_file = (bin, mime) => {
  273. if ( typeof bin === 'string' ) {
  274. return new Blob([bin], { type: mime });
  275. }
  276. return bin;
  277. };
  278. const fd = new FormData();
  279. fd.append('original_client_socket_id', '');
  280. fd.append('socket_id', '');
  281. fd.append('operation_id', '');
  282. let fileI = 0;
  283. for ( let i=0 ; i < ops.length ; i++ ) {
  284. const op = ops[i];
  285. fd.append('operation', JSON.stringify(op));
  286. }
  287. const files = [];
  288. for ( let i=0 ; i < ops.length ; i++ ) {
  289. const op = ops[i];
  290. if ( op.op === 'mkdir' ) continue;
  291. if ( op.op === 'mktree' ) continue;
  292. let mime = op.mime ?? 'text/plain';
  293. const file = adapt_file(bins[fileI++], mime);
  294. fd.append('fileinfo', JSON.stringify({
  295. size: file.size,
  296. name: op.name,
  297. mime,
  298. }));
  299. files.push({
  300. op, file,
  301. })
  302. delete op.name;
  303. }
  304. for ( const file of files ) {
  305. const { op, file: blob } = file;
  306. fd.append('file', blob, op.name);
  307. }
  308. const res = await axios.request({
  309. httpsAgent: this.httpsAgent,
  310. method: 'post',
  311. url: this.getURL(ep),
  312. data: fd,
  313. headers: {
  314. ...this.headers_,
  315. 'Content-Type': 'multipart/form-data'
  316. },
  317. });
  318. return res.data.results;
  319. }
  320. batch_json (ep, ops, bins) {
  321. return axios.request({
  322. httpsAgent: this.httpsAgent,
  323. method: 'post',
  324. url: this.getURL(ep),
  325. data: ops,
  326. headers: {
  327. ...this.headers_,
  328. 'Content-Type': 'application/json',
  329. },
  330. });
  331. }
  332. }