yukicoder Easy Test

Make testing sample cases easy

  1. // ==UserScript==
  2. // @name yukicoder Easy Test
  3. // @namespace http://yukicoder.me/
  4. // @version 0.1.1
  5. // @description Make testing sample cases easy
  6. // @author magurofly
  7. // @match https://yukicoder.me/problems/no/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // This script uses variables from page below:
  12. // * `$`
  13. // * `ace`
  14.  
  15. // This scripts consists of three modules:
  16. // * bottom menu
  17. // * code runner
  18. // * view
  19.  
  20. (function script() {
  21.  
  22. const VERSION = "0.1.0";
  23.  
  24. if (typeof unsafeWindow !== "undefined") {
  25. console.log(unsafeWindow);
  26. unsafeWindow.eval(`(${script})();`);
  27. console.log("Script run in unsafeWindow");
  28. return;
  29. }
  30. const $ = window.$;
  31. const getSourceCode = window.getSourceCode;
  32. const csrfToken = window.csrfToken;
  33.  
  34. // -- code runner --
  35. const codeRunner = (function() {
  36. 'use strict';
  37.  
  38. function buildParams(data) {
  39. return Object.entries(data).map(([key, value]) =>
  40. encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  41. }
  42.  
  43. function sleep(ms) {
  44. return new Promise(done => setTimeout(done, ms));
  45. }
  46.  
  47. class CodeRunner {
  48. constructor(label, site) {
  49. this.label = site ? `${label} [${site}]` : label;
  50. }
  51.  
  52. async test(sourceCode, input, supposedOutput, options) {
  53. const result = await this.run(sourceCode, input);
  54. if (result.status != "OK" || typeof supposedOutput !== "string") return result;
  55. let output = result.stdout || "";
  56.  
  57. if (options.trim) {
  58. supposedOutput = supposedOutput.trim();
  59. output = output.trim();
  60. }
  61.  
  62. let equals = (x, y) => x === y;
  63.  
  64. if ("allowableError" in options) {
  65. const floatPattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
  66. const superEquals = equals;
  67. equals = (x, y) => {
  68. if (floatPattern.test(x) && floatPattern.test(y)) return Math.abs(parseFloat(x) - parseFloat(y)) <= options.allowableError;
  69. return superEquals(x, y);
  70. }
  71. }
  72.  
  73. if (options.split) {
  74. const superEquals = equals;
  75. equals = (x, y) => {
  76. x = x.split(/\s+/);
  77. y = y.split(/\s+/);
  78. if (x.length != y.length) return false;
  79. const len = x.length;
  80. for (let i = 0; i < len; i++) {
  81. if (!superEquals(x[i], y[i])) return false;
  82. }
  83. return true;
  84. }
  85. }
  86.  
  87. if (equals(output, supposedOutput)) {
  88. result.status = "AC";
  89. } else {
  90. result.status = "WA";
  91. }
  92.  
  93. return result;
  94. }
  95. }
  96.  
  97. class WandboxRunner extends CodeRunner {
  98. constructor(name, label, options = {}) {
  99. super(label, "Wandbox");
  100. this.name = name;
  101. }
  102.  
  103. run(sourceCode, input) {
  104. let options = this.options;
  105. if (typeof options == "function") options = options(sourceCode, input);
  106. return this.request(Object.assign(JSON.stringify({
  107. compiler: this.name,
  108. code: sourceCode,
  109. stdin: input,
  110. }), this.options));
  111. }
  112.  
  113. async request(body) {
  114. const startTime = Date.now();
  115. let res;
  116. try {
  117. res = await fetch("https://wandbox.org/api/compile.json", {
  118. method: "POST",
  119. mode: "cors",
  120. headers: {
  121. "Content-Type": "application/json",
  122. },
  123. body,
  124. }).then(r => r.json());
  125. } catch (error) {
  126. return {
  127. status: "IE",
  128. stderr: error,
  129. };
  130. }
  131. const endTime = Date.now();
  132.  
  133. const result = {
  134. status: "OK",
  135. exitCode: res.status,
  136. execTime: endTime - startTime,
  137. stdout: res.program_output,
  138. stderr: res.program_error,
  139. };
  140. if (res.status != 0) {
  141. if (res.signal) {
  142. result.exitCode += " (" + res.signal + ")";
  143. }
  144. result.stdout = (res.compiler_output || "") + (result.stdout || "");
  145. result.stderr = (res.compiler_error || "") + (result.stderr || "");
  146. if (res.compiler_output || res.compiler_error) {
  147. result.status = "CE";
  148. } else {
  149. result.status = "RE";
  150. }
  151. }
  152.  
  153. return result;
  154. }
  155. }
  156.  
  157. class PaizaIORunner extends CodeRunner {
  158. constructor(name, label) {
  159. super(label, "PaizaIO");
  160. this.name = name;
  161. }
  162.  
  163. async run(sourceCode, input) {
  164. let id, status, error;
  165. try {
  166. const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
  167. source_code: sourceCode,
  168. language: this.name,
  169. input,
  170. longpoll: true,
  171. longpoll_timeout: 10,
  172. api_key: "guest",
  173. }), {
  174. method: "POST",
  175. mode: "cors",
  176. }).then(r => r.json());
  177. id = res.id;
  178. status = res.status;
  179. error = res.error;
  180. } catch (error) {
  181. return {
  182. status: "IE",
  183. stderr: error,
  184. };
  185. }
  186.  
  187. while (status == "running") {
  188. const res = await (await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
  189. id,
  190. api_key: "guest",
  191. }), {
  192. mode: "cors",
  193. })).json();
  194. status = res.status;
  195. error = res.error;
  196. }
  197.  
  198. const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
  199. id,
  200. api_key: "guest",
  201. }), {
  202. mode: "cors",
  203. }).then(r => r.json());
  204.  
  205. const result = {
  206. exitCode: res.exit_code,
  207. execTime: +res.time * 1e3,
  208. memory: +res.memory * 1e-3,
  209. };
  210.  
  211. if (res.build_result == "failure") {
  212. result.status = "CE";
  213. result.exitCode = res.build_exit_code;
  214. result.stdout = res.build_stdout;
  215. result.stderr = res.build_stderr;
  216. } else {
  217. result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
  218. result.exitCode = res.exit_code;
  219. result.stdout = res.stdout;
  220. result.stderr = res.stderr;
  221. }
  222.  
  223. return result;
  224. }
  225. }
  226.  
  227. class CustomRunner extends CodeRunner {
  228. constructor(label, fn) {
  229. super(label, null);
  230. this.fn = fn;
  231. }
  232.  
  233. async run(sourceCode, input) {
  234. return this.fn(sourceCode, input);
  235. }
  236. }
  237.  
  238. const loader = {
  239. loaded: {},
  240. async load(url, options = { mode: "cors", }) {
  241. if (!(url in this.loaded)) {
  242. this.loaded[url] = await fetch(url, options);
  243. }
  244. return this.loaded[url];
  245. },
  246. };
  247.  
  248. const runners = {
  249. cpp14: [new WandboxRunner("gcc-10.1.0", "C++14 (gcc 10.1.0 + boost 1.73.0)", {options: "warning,boost-1.73.0-gcc-10.1.0,c++14,cpp-no-pedantic"})],
  250. cpp17: [new WandboxRunner("gcc-10.1.0", "C++17 (gcc 10.1.0 + boost 1.73.0)", {options: "warning,boost-1.73.0-gcc-10.1.0,c++17,cpp-no-pedantic"})],
  251. "cpp-clang": [
  252. new WandboxRunner("clang-7.0.0", "C++17 (clang 7.0.0 &#43; boost 1.73.0)", {options: "warning,boost-1.73.0-gcc-10.1.0,c++17,cpp-no-pedantic"}),
  253. new PaizaIORunner("c", "C (C17 / Clang 10.0.0)"),
  254. ],
  255. cpp17: [new WandboxRunner("gcc-4.8.5", "C++11 (gcc 4.8.5)", {options: "warning,boost-nothing-gcc-4.8.5,c++11,cpp-no-pedantic"})],
  256. c11: [new WandboxRunner("gcc-10.1.0-c", "C (gcc 10.1.0)", {options: "warning,c11,cpp-no-pedantic"})],
  257. c: [new WandboxRunner("gcc-4.8.5-c", "C99 (gcc 4.8.5)", {options: "warning,c99,cpp-no-pedantic"})],
  258. java8: [new WandboxRunner("openjdk-jdk-11+28", "Java11 (openjdk 11.0.7)")],
  259. csharp: [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
  260. csharp_mono: [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
  261. perl: [new WandboxRunner("perl-5.18.4", "Perl (5.18.4)")],
  262. // perl6: ?,
  263. php: [new WandboxRunner("php-5.5.6", "PHP (5.5.6)")],
  264. php7: [new WandboxRunner("php-7.3.3", "PHP7 (7.3.3)")],
  265. python3: [new WandboxRunner("cpython-3.8.0", "Python3 (3.8.0)")],
  266. pypy2: [new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)")],
  267. pypy3: [new WandboxRunner("pypy-7.2.0-3", "PyPy2 (7.2.0)")],
  268. ruby: [
  269. new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  270. new WandboxRunner("ruby-2.7.0-preview1", "Ruby (2.7.0-preview1)"),
  271. ],
  272. d: [new WandboxRunner("dmd-head", "D (dmd 2.0.87.0)")],
  273. go: [new WandboxRunner("go-1.14-2", "Go (1.14.2)")],
  274. haskell: [new WandboxRunner("ghc-head", "Haskell (8.7.20181121)")],
  275. scala: [new WandboxRunner("scala-2.13.x", "Scala (2.13.x)")],
  276. nim: [new WandboxRunner("nim-1.2.0", "Nim (1.2.0)")],
  277. rust: [new WandboxRunner("rust-head", "Rust (1.37.0-dev)")],
  278. // kotlin: ?,
  279. // scheme: ?,
  280. crystal: [new WandboxRunner("crystal-0.24.1", "Crystal (0.24.1)")],
  281. swift: [new WandboxRunner("swift-head", "Swift (5.3-dev)")],
  282. ocaml: [new WandboxRunner("ocaml-4.05.0", "OCaml (4.05.0)", {options: "ocaml-core"})],
  283. // clojure: [new WandboxRunner("", "")],
  284. fsharp: [new WandboxRunner("fsharp-4.1.34", "F# (4.1.34)")],
  285. elixir: [new WandboxRunner("elixir-head", "Elixir (1.12.0-dev)")],
  286. lua: [new WandboxRunner("luajit-2.0.5", "Lua (LuaJIT 2.0.5)")],
  287. // fortran: ?,
  288. node: [new WandboxRunner("nodejs-14.0.0", "JavaScript (Node.js v14.0.0)")],
  289. typescript: [new WandboxRunner("typescript-3.9.5", "TypeScript (typescript 3.9.5)")],
  290. lisp: [new WandboxRunner("sbcl-1.3.18", "Common Lisp (sbcl 1.3.18)")],
  291. // kuin: ?,
  292. // kuinexe: ?,
  293. vim: [new WandboxRunner("vim-head", "Vim script (v8.2.2017)")],
  294. sh: [new WandboxRunner("bash", "Bash (Bash 4.3.48(1)-release)")],
  295. // nasm: ?,
  296. // clay: ?,
  297. // bf: ?,
  298. // Whitespace: ?,
  299. text: [new CustomRunner("Text", (sourceCode, input) => ({
  300. status: "OK",
  301. exitCode: 0,
  302. stdout: sourceCode,
  303. }))],
  304. };
  305.  
  306. console.info("codeRunner OK");
  307.  
  308. return {
  309. run(languageId, index, sourceCode, input, supposedOutput = null, options = { trim: true, split: true, }) {
  310. if (!(languageId in runners)) {
  311. return Promise.reject("language not supported");
  312. }
  313. return runners[languageId][index].test(sourceCode, input, supposedOutput, options);
  314. },
  315.  
  316. getEnvironment(languageId) {
  317. if (!(languageId in runners)) {
  318. return Promise.reject("language not supported");
  319. }
  320. return Promise.resolve(runners[languageId].map(runner => runner.label));
  321. },
  322. };
  323. })();
  324.  
  325.  
  326. // -- bottom menu --
  327. const bottomMenu = (function () {
  328. 'use strict';
  329.  
  330. const tabs = new Set();
  331.  
  332. const bottomMenuKey = $(`<button id="bottom-menu-key" type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bottom-menu">`);
  333. const bottomMenuTabs = $(`<ul id="bottom-menu-tabs" class="nav nav-tabs">`);
  334. const bottomMenuContents = $(`<div id="bottom-menu-contents" class="tab-content">`);
  335.  
  336. $(() => {
  337. $(`<style>`)
  338. .text(`
  339.  
  340. #bottom-menu-wrapper {
  341. background: transparent;
  342. border: none;
  343. pointer-events: none;
  344. padding: 0;
  345. font-size: 10pt;
  346. }
  347.  
  348. #bottom-menu-wrapper>.container {
  349. position: absolute;
  350. bottom: 0;
  351. width: 100%;
  352. padding: 0;
  353. }
  354.  
  355. #bottom-menu-wrapper>.container>.navbar-header {
  356. float: none;
  357. }
  358.  
  359. #bottom-menu-key {
  360. display: block;
  361. float: none;
  362. margin: 0 auto;
  363. padding: 10px 3em;
  364. border-radius: 5px 5px 0 0;
  365. background: #000;
  366. opacity: 0.85;
  367. color: #FFF;
  368. cursor: pointer;
  369. pointer-events: auto;
  370. text-align: center;
  371. font-size: 12pt;
  372. }
  373.  
  374. #bottom-menu-key:before {
  375. content: "▽";
  376. }
  377.  
  378. #bottom-menu-key.collapsed:before {
  379. content: "△";
  380. }
  381.  
  382. #bottom-menu-tabs {
  383. padding: 3px 0 0 10px;
  384. cursor: n-resize;
  385. }
  386.  
  387. #bottom-menu-tabs a {
  388. pointer-events: auto;
  389. }
  390.  
  391. #bottom-menu {
  392. pointer-events: auto;
  393. background: rgba(0, 0, 0, 0.8);
  394. color: #fff;
  395. max-height: unset;
  396. }
  397.  
  398. #bottom-menu.collapse:not(.in) {
  399. display: none !important;
  400. }
  401.  
  402. #bottom-menu-tabs>li>a {
  403. background: rgba(100, 100, 100, 0.5);
  404. border: solid 1px #ccc;
  405. color: #fff;
  406. }
  407.  
  408. #bottom-menu-tabs>li>a:hover {
  409. background: rgba(150, 150, 150, 0.5);
  410. border: solid 1px #ccc;
  411. color: #333;
  412. }
  413.  
  414. #bottom-menu-tabs>li.active>a {
  415. background: #eee;
  416. border: solid 1px #ccc;
  417. color: #333;
  418. }
  419.  
  420. .bottom-menu-btn-close {
  421. font-size: 8pt;
  422. vertical-align: baseline;
  423. padding: 0 0 0 6px;
  424. margin-right: -6px;
  425. }
  426.  
  427. #bottom-menu-contents {
  428. padding: 5px 15px;
  429. max-height: 50vh;
  430. overflow-y: auto;
  431. }
  432.  
  433. #bottom-menu-contents .panel {
  434. color: #333;
  435. }
  436.  
  437. `)
  438. .appendTo("head");
  439. const bottomMenu = $(`<div id="bottom-menu" class="collapse navbar-collapse">`).append(bottomMenuTabs, bottomMenuContents);
  440. $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
  441. .append($(`<div class="container">`)
  442. .append(
  443. $(`<div class="navbar-header">`).append(bottomMenuKey),
  444. bottomMenu))
  445. .appendTo("#body");
  446.  
  447. let resizeStart = null;
  448. bottomMenuTabs.on({
  449. mousedown({target, pageY}) {
  450. if (!$(target).is("#bottom-menu-tabs")) return;
  451. resizeStart = {y: pageY, height: bottomMenuContents.height()};
  452. },
  453. mousemove(e) {
  454. if (!resizeStart) return;
  455. e.preventDefault();
  456. bottomMenuContents.height(resizeStart.height - (e.pageY - resizeStart.y));
  457. },
  458. });
  459. $(document).on({
  460. mouseup() {
  461. resizeStart = null;
  462. },
  463. mouseleave() {
  464. resizeStart = null;
  465. },
  466. });
  467. });
  468.  
  469. const menuController = {
  470. addTab(tabId, tabLabel, paneContent, options = {}) {
  471. console.log("addTab: %s (%s)", tabLabel, tabId, paneContent);
  472. const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
  473. .click(e => {
  474. e.preventDefault();
  475. tab.tab("show");
  476. })
  477. .append(tabLabel);
  478. const tabLi = $(`<li>`).append(tab).appendTo(bottomMenuTabs);
  479. const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo(bottomMenuContents);
  480. console.dirxml(bottomMenuContents);
  481. const controller = {
  482. close() {
  483. tabLi.remove();
  484. pane.remove();
  485. tabs.delete(tab);
  486. if (tabLi.hasClass("active") && tabs.size > 0) {
  487. tabs.values().next().value.tab("show");
  488. }
  489. },
  490.  
  491. show() {
  492. menuController.show();
  493. tab.tab("show");
  494. }
  495. };
  496. tabs.add(tab);
  497. if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
  498. if (options.active || tabs.size == 1) pane.ready(() => tab.tab("show"));
  499. return controller;
  500. },
  501.  
  502. show() {
  503. if (bottomMenuKey.hasClass("collapsed")) bottomMenuKey.click();
  504. },
  505. };
  506.  
  507. console.info("bottomMenu OK");
  508.  
  509. return menuController;
  510. })();
  511.  
  512. $(() => {
  513. // external elements
  514. const E = {
  515. lang: $("#lang"),
  516. editor: ace.edit("rich_source"),
  517. sourceObject: $("#source"),
  518. sample: $(".sample"),
  519. };
  520.  
  521. function getSourceCode() {
  522. if (E.sourceObject.is(":visible")) return E.sourceObject.val();
  523. return E.editor.getSession().getValue();
  524. }
  525.  
  526. async function runTest(title, input, output = null) {
  527. const uid = Date.now().toString();
  528. title = title ? "Result " + title : "Result";
  529. const content = $(`<div class="container">`)
  530. .html(`
  531. <div class="row"><div class="form-group">
  532. <label class="control-label col-sm-2" for="yukicoder-easy-test-${uid}-stdin">Standard Input</label>
  533. <div class="col-sm-8">
  534. <textarea id="yukicoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  535. </div>
  536. </div></div>
  537. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  538. <div class="panel panel-default"><table class="table table-bordered">
  539. <tr id="yukicoder-easy-test-${uid}-row-exit-code">
  540. <th class="text-center">Exit Code</th>
  541. <td id="yukicoder-easy-test-${uid}-exit-code" class="text-right"></td>
  542. </tr>
  543. <tr id="yukicoder-easy-test-${uid}-row-exec-time">
  544. <th class="text-center">Exec Time</th>
  545. <td id="yukicoder-easy-test-${uid}-exec-time" class="text-right"></td>
  546. </tr>
  547. <tr id="yukicoder-easy-test-${uid}-row-memory">
  548. <th class="text-center">Memory</th>
  549. <td id="yukicoder-easy-test-${uid}-memory" class="text-right"></td>
  550. </tr>
  551. </table></div>
  552. </div></div>
  553. <div class="row"><div class="form-group">
  554. <label class="control-label col-sm-2" for="yukicoder-easy-test-${uid}-stdout">Standard Output</label>
  555. <div class="col-sm-8">
  556. <textarea id="yukicoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  557. </div>
  558. </div></div>
  559. <div class="row"><div class="form-group">
  560. <label class="control-label col-sm-2" for="yukicoder-easy-test-${uid}-stderr">Standard Error</label>
  561. <div class="col-sm-8">
  562. <textarea id="yukicoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  563. </div>
  564. </div></div>
  565. `);
  566. const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  567. $(`#yukicoder-easy-test-${uid}-stdin`).val(input);
  568.  
  569. const options = { trim: true, split: true, };
  570. if ($("#yukicoder-easy-test-allowable-error-check").prop("checked")) {
  571. options.allowableError = parseFloat($("#yukicoder-easy-test-allowable-error").val());
  572. }
  573.  
  574. const lang = E.lang.val();
  575. const result = await codeRunner.run(lang, +$("#yukicoder-easy-test-language").val(), getSourceCode(), input, output, options);
  576.  
  577. $(`#yukicoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0).toggleClass("bg-success", result.exitCode == 0);
  578. $(`#yukicoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  579. if ("execTime" in result) $(`#yukicoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  580. if ("memory" in result) $(`#yukicoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  581. $(`#yukicoder-easy-test-${uid}-stdout`).val(result.stdout);
  582. $(`#yukicoder-easy-test-${uid}-stderr`).val(result.stderr);
  583.  
  584. result.uid = uid;
  585. result.tab = tab;
  586. return result;
  587. }
  588.  
  589. console.log("bottomMenu", bottomMenu);
  590.  
  591. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="yukicoder-easy-test-container" class="form-horizontal">`)
  592. .html(`
  593. <small style="position: absolute; bottom: 0; right: 0;">yukicoder Easy Test v${VERSION}</small>
  594. <div class="row">
  595. <div class="col-12 col-md-10">
  596. <div class="form-group">
  597. <label class="control-label col-sm-2">Test Environment</label>
  598. <div class="col-sm-8">
  599. <select class="form-control" id="yukicoder-easy-test-language"></select>
  600. <!--input id="yukicoder-easy-test-language" type="text" class="form-control" readonly-->
  601. </div>
  602. </div>
  603. </div>
  604. </div>
  605. <div class="row">
  606. <div class="col-12 col-md-10">
  607. <div class="form-group">
  608. <label class="control-label col-sm-2" for="yukicoder-easy-test-allowable-error-check">Allowable Error</label>
  609. <div class="col-sm-8">
  610. <div class="input-group">
  611. <span class="input-group-addon">
  612. <input id="yukicoder-easy-test-allowable-error-check" type="checkbox" checked>
  613. </span>
  614. <input id="yukicoder-easy-test-allowable-error" type="text" class="form-control" value="1e-6">
  615. </div>
  616. </div>
  617. </div>
  618. </div>
  619. </div>
  620. <div class="row">
  621. <div class="col-12 col-md-10">
  622. <div class="form-group">
  623. <label class="control-label col-sm-2" for="yukicoder-easy-test-input">Standard Input</label>
  624. <div class="col-sm-8">
  625. <textarea id="yukicoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  626. </div>
  627. </div>
  628. </div>
  629. <div class="col-12 col-md-4">
  630. <label class="control-label col-sm-2"></label>
  631. <div class="form-group">
  632. <div class="col-sm-8">
  633. <a id="yukicoder-easy-test-run" class="btn btn-primary">Run</a>
  634. </div>
  635. </div>
  636. </div>
  637. </div>
  638. <style>
  639. #yukicoder-easy-test-language {
  640. border: none;
  641. background: transparent;
  642. font: inherit;
  643. color: #fff;
  644. }
  645. #yukicoder-easy-test-language option {
  646. border: none;
  647. color: #333;
  648. font: inherit;
  649. }
  650. </style>
  651. `).ready(() => {
  652. $("#yukicoder-easy-test-run").click(() => runTest("", $("#yukicoder-easy-test-input").val()));
  653. E.lang.on("change", () => setLanguage());
  654. $("#yukicoder-easy-test-allowable-error").attr("disabled", this.checked);
  655. $("#yukicoder-easy-test-allowable-error-check").on("change", function () {
  656. $("#yukicoder-easy-test-allowable-error").attr("disabled", !this.checked);
  657. });
  658.  
  659. function setLanguage() {
  660. const languageId = E.lang.val();
  661. codeRunner.getEnvironment(languageId).then(labels => {
  662. console.log(`language: ${labels[0]} (${languageId})`);
  663. $("#yukicoder-easy-test-language").css("color", "#fff").empty().append(labels.map((label, index) => $(`<option>`).val(index).text(label)));
  664. $("#yukicoder-easy-test-run").removeClass("disabled");
  665. $("#yukicoder-easy-test-btn-test-all").attr("disabled", false);
  666. }, error => {
  667. console.log(`language: ? (${languageId})`);
  668. $("#yukicoder-easy-test-language").css("color", "#f55").empty().append($(`<option>`).text(error));
  669. $("#yukicoder-easy-test-run").addClass("disabled");
  670. $("#yukicoder-easy-test-btn-test-all").attr("disabled", true);
  671. });
  672. }
  673.  
  674. setLanguage();
  675. }), { active: true });
  676.  
  677. const testfuncs = [];
  678.  
  679. for (const sample of E.sample) {
  680. const title = $(sample).find("h5").text();
  681. const [input, output] = $(sample).find("pre");
  682. const testfunc = async () => {
  683. const result = await runTest(title, input.textContent, output.textContent);
  684. if (result.status == "OK" || result.status == "AC") {
  685. $(`#yukicoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  686. }
  687. return result;
  688. };
  689. testfuncs.push(testfunc);
  690.  
  691. const runButton = $(`<a class="btn btn-primary btn-sm" style="margin-left: 0.5em">`)
  692. .text("Run")
  693. .click(async () => {
  694. await testfunc();
  695. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  696. });
  697. $(sample).find(".copy-sample-input").after(runButton);
  698. }
  699.  
  700. const testAllResultRow = $(`<div class="row">`);
  701. const testAllButton = $(`<a id="yukicoder-easy-test-btn-test-all" class="btn btn-default btn-sm" style="margin-left: 5px">`)
  702. .text("Test All Samples")
  703. .click(async () => {
  704. if (testAllButton.attr("disabled")) throw new Error("Button is disabled");
  705. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  706. const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
  707. let finished = 0;
  708. const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  709. .append($(`<span aria-hidden="true">`).text("\xd7"));
  710. const resultAlert = $(`<div class="alert alert-dismissible">`)
  711. .append(closeButton)
  712. .append($(`<div class="progress">`).append(progress))
  713. .append(...statuses)
  714. .appendTo(testAllResultRow);
  715. const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
  716. const result = await testfunc();
  717. finished++;
  718. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  719. statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
  720. return result;
  721. }));
  722. if (results.every(({status}) => status == "AC")) {
  723. resultAlert.addClass("alert-success");
  724. } else {
  725. resultAlert.addClass("alert-warning");
  726. }
  727. closeButton.click(() => {
  728. for (const {tab} of results) {
  729. tab.close();
  730. }
  731. });
  732. });
  733. $("#submit_form input[type='submit']").after(testAllButton).closest("form").append(testAllResultRow);
  734.  
  735. console.info("view OK");
  736. });
  737.  
  738. })();