AtCoder Easy Test

Make testing sample cases easy

2020-12-23 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name AtCoder Easy Test
  3. // @namespace http://atcoder.jp/
  4. // @version 1.4.1
  5. // @description Make testing sample cases easy
  6. // @author magurofly
  7. // @match https://atcoder.jp/contests/*/tasks/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // This script uses variables from page below:
  12. // * `$`
  13. // * `getSourceCode`
  14. // * `csrfToken`
  15.  
  16. // This scripts consists of three modules:
  17. // * bottom menu
  18. // * code runner
  19. // * view
  20.  
  21. (function script() {
  22.  
  23. const VERSION = "1.4.1";
  24.  
  25. if (typeof unsafeWindow !== "undefined") {
  26. console.log(unsafeWindow);
  27. unsafeWindow.eval(`(${script})();`);
  28. console.log("Script run in unsafeWindow");
  29. return;
  30. }
  31. const $ = window.$;
  32. const getSourceCode = window.getSourceCode;
  33. const csrfToken = window.csrfToken;
  34.  
  35. // -- code runner --
  36. const codeRunner = (function() {
  37. 'use strict';
  38.  
  39. function buildParams(data) {
  40. return Object.entries(data).map(([key, value]) =>
  41. encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  42. }
  43.  
  44. function sleep(ms) {
  45. return new Promise(done => setTimeout(done, ms));
  46. }
  47.  
  48. class CodeRunner {
  49. constructor(label, site) {
  50. this.label = `${label} [${site}]`;
  51. }
  52.  
  53. async test(sourceCode, input, supposedOutput, options) {
  54. const result = await this.run(sourceCode, input);
  55. if (result.status != "OK" || typeof supposedOutput !== "string") return result;
  56. let output = result.stdout || "";
  57.  
  58. if (options.trim) {
  59. supposedOutput = supposedOutput.trim();
  60. output = output.trim();
  61. }
  62.  
  63. let equals = (x, y) => x === y;
  64.  
  65. if ("allowableError" in options) {
  66. const floatPattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
  67. const superEquals = equals;
  68. equals = (x, y) => {
  69. if (floatPattern.test(x) && floatPattern.test(y)) return Math.abs(parseFloat(x) - parseFloat(y)) <= options.allowableError;
  70. return superEquals(x, y);
  71. }
  72. }
  73.  
  74. if (options.split) {
  75. const superEquals = equals;
  76. equals = (x, y) => {
  77. x = x.split(/\s+/);
  78. y = y.split(/\s+/);
  79. if (x.length != y.length) return false;
  80. const len = x.length;
  81. for (let i = 0; i < len; i++) {
  82. if (!superEquals(x[i], y[i])) return false;
  83. }
  84. return true;
  85. }
  86. }
  87.  
  88. if (equals(output, supposedOutput)) {
  89. result.status = "AC";
  90. } else {
  91. result.status = "WA";
  92. }
  93.  
  94. return result;
  95. }
  96. }
  97.  
  98. class CustomRunner extends CodeRunner {
  99. constructor(label, run) {
  100. super(label, "Browser");
  101. this.run = run;
  102. }
  103. }
  104.  
  105. class WandboxRunner extends CodeRunner {
  106. constructor(name, label, options = {}) {
  107. super(label, "Wandbox");
  108. this.name = name;
  109. this.options = options;
  110. }
  111.  
  112. run(sourceCode, input) {
  113. let options = this.options;
  114. if (typeof options == "function") options = options(sourceCode, input);
  115. return this.request(Object.assign(JSON.stringify({
  116. compiler: this.name,
  117. code: sourceCode,
  118. stdin: input,
  119. }), this.options));
  120. }
  121.  
  122. async request(body) {
  123. const startTime = Date.now();
  124. let res;
  125. try {
  126. res = await fetch("https://wandbox.org/api/compile.json", {
  127. method: "POST",
  128. mode: "cors",
  129. headers: {
  130. "Content-Type": "application/json",
  131. },
  132. body,
  133. }).then(r => r.json());
  134. } catch (error) {
  135. return {
  136. status: "IE",
  137. stderr: error,
  138. };
  139. }
  140. const endTime = Date.now();
  141.  
  142. const result = {
  143. status: "OK",
  144. exitCode: res.status,
  145. execTime: endTime - startTime,
  146. stdout: res.program_output,
  147. stderr: res.program_error,
  148. };
  149. if (res.status != 0) {
  150. if (res.signal) {
  151. result.exitCode += " (" + res.signal + ")";
  152. }
  153. result.stdout = (res.compiler_output || "") + (result.stdout || "");
  154. result.stderr = (res.compiler_error || "") + (result.stderr || "");
  155. if (res.compiler_output || res.compiler_error) {
  156. result.status = "CE";
  157. } else {
  158. result.status = "RE";
  159. }
  160. }
  161.  
  162. return result;
  163. }
  164. }
  165.  
  166. class PaizaIORunner extends CodeRunner {
  167. constructor(name, label) {
  168. super(label, "PaizaIO");
  169. this.name = name;
  170. }
  171.  
  172. async run(sourceCode, input) {
  173. let id, status, error;
  174. try {
  175. const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
  176. source_code: sourceCode,
  177. language: this.name,
  178. input,
  179. longpoll: true,
  180. longpoll_timeout: 10,
  181. api_key: "guest",
  182. }), {
  183. method: "POST",
  184. mode: "cors",
  185. }).then(r => r.json());
  186. id = res.id;
  187. status = res.status;
  188. error = res.error;
  189. } catch (error) {
  190. return {
  191. status: "IE",
  192. stderr: error,
  193. };
  194. }
  195.  
  196. while (status == "running") {
  197. const res = await (await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
  198. id,
  199. api_key: "guest",
  200. }), {
  201. mode: "cors",
  202. })).json();
  203. status = res.status;
  204. error = res.error;
  205. }
  206.  
  207. const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
  208. id,
  209. api_key: "guest",
  210. }), {
  211. mode: "cors",
  212. }).then(r => r.json());
  213.  
  214. const result = {
  215. exitCode: res.exit_code,
  216. execTime: +res.time * 1e3,
  217. memory: +res.memory * 1e-3,
  218. };
  219.  
  220. if (res.build_result == "failure") {
  221. result.status = "CE";
  222. result.exitCode = res.build_exit_code;
  223. result.stdout = res.build_stdout;
  224. result.stderr = res.build_stderr;
  225. } else {
  226. result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
  227. result.exitCode = res.exit_code;
  228. result.stdout = res.stdout;
  229. result.stderr = res.stderr;
  230. }
  231.  
  232. return result;
  233. }
  234. }
  235.  
  236. const loader = {
  237. loaded: {},
  238. async load(url, options = { mode: "cors", }) {
  239. if (!(url in this.loaded)) {
  240. this.loaded[url] = await fetch(url, options);
  241. }
  242. return this.loaded[url];
  243. },
  244. };
  245.  
  246. class WandboxCppRunner extends WandboxRunner {
  247. async run(sourceCode, input) {
  248. const allFiles = ["convolution", "dsu", "fenwicktree", "internal_bit", "internal_math", "internal_queue", "internal_scc", "internal_type_traits", "lazysegtree", "math", "maxflow", "mincostflow", "modint", "scc", "segtree", "string", "twosat"];
  249. const files = new Set(["all"].concat(allFiles).filter(file => new RegExp(String.raw`^#\s*include\s*<atcoder/${file}>`, "m").test(sourceCode)));
  250. let allHeaders = false;
  251. if (files.has("all")) {
  252. allHeaders = true;
  253. files.delete("all");
  254. for (const file of allFiles) {
  255. files.add(file);
  256. }
  257. }
  258.  
  259. if (files.has("convolution")) {
  260. files.add("modint");
  261. }
  262. if (files.has("convolution") || files.has("lazysegtree") || files.has("segtree")) {
  263. files.add("internal_bit");
  264. }
  265. if (files.has("maxflow")) {
  266. files.add("internal_queue");
  267. }
  268. if (files.has("scc") || files.has("twosat")) {
  269. files.add("internal_scc");
  270. }
  271. if (files.has("math") || files.has("modint")) {
  272. files.add("internal_math");
  273. }
  274. if (files.has("fenwicktree") || files.has("fenwicktree")) {
  275. files.add("internal_type_traits");
  276. }
  277.  
  278. const ACLBase = "https://cdn.jsdelivr.net/gh/atcoder/ac-library/atcoder/";
  279.  
  280. const codePromises = [];
  281. const codes = [];
  282. for (const file of files) {
  283. codePromises.push(fetch(ACLBase + file + ".hpp", {
  284. mode: "cors",
  285. cache: "force-cache",
  286. })
  287. .then(r => r.text())
  288. .then(code => codes.push({ file: "atcoder/" + file, code })));
  289. }
  290. if (allHeaders) {
  291. codePromises.push(fetch(ACLBase + "all", {
  292. mode: "cors",
  293. cache: "force-cache",
  294. })
  295. .then(r => r.text())
  296. .then(code => codes.push({ file: "atcoder/all", code })));
  297. }
  298.  
  299. await Promise.all(codePromises);
  300.  
  301.  
  302. let options = this.options;
  303. if (typeof options == "function") options = options(sourceCode, input);
  304. return await this.request(JSON.stringify(Object.assign({
  305. compiler: this.name,
  306. code: sourceCode,
  307. stdin: input,
  308. codes,
  309. "compiler-option-raw": "-I.",
  310. }, options)));
  311. }
  312. }
  313.  
  314. let waitAtCoderCustomTest = Promise.resolve();
  315. const AtCoderCustomTestBase = location.href.replace(/\/tasks\/.+$/, "/custom_test");
  316. const AtCoderCustomTestResultAPI = AtCoderCustomTestBase + "/json?reload=true";
  317. const AtCoderCustomTestSubmitAPI = AtCoderCustomTestBase + "/submit/json";
  318. class AtCoderRunner extends CodeRunner {
  319. constructor(languageId, label) {
  320. super(label, "AtCoder");
  321. this.languageId = languageId;
  322. }
  323.  
  324. async run(sourceCode, input) {
  325. const promise = this.submit(sourceCode, input);
  326. waitAtCoderCustomTest = promise;
  327. return await promise;
  328. }
  329.  
  330. async submit(sourceCode, input) {
  331. try {
  332. await waitAtCoderCustomTest;
  333. } catch (error) {
  334. console.error(error);
  335. }
  336.  
  337. const error = await fetch(AtCoderCustomTestSubmitAPI, {
  338. method: "POST",
  339. credentials: "include",
  340. headers: {
  341. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  342. },
  343. body: buildParams({
  344. "data.LanguageId": this.languageId,
  345. sourceCode,
  346. input,
  347. csrf_token: csrfToken,
  348. }),
  349. }).then(r => r.text());
  350.  
  351. if (error) {
  352. throw new Error(error)
  353. }
  354.  
  355. await sleep(100);
  356.  
  357. for (;;) {
  358. const data = await fetch(AtCoderCustomTestResultAPI, {
  359. method: "GET",
  360. credentials: "include",
  361. }).then(r => r.json());
  362.  
  363. if (!("Result" in data)) continue;
  364. const result = data.Result;
  365.  
  366. if ("Interval" in data) {
  367. await sleep(data.Interval);
  368. continue;
  369. }
  370.  
  371. return {
  372. status: (result.ExitCode == 0) ? "OK" : (result.TimeConsumption == -1) ? "CE" : "RE",
  373. exitCode: result.ExitCode,
  374. execTime: result.TimeConsumption,
  375. memory: result.MemoryConsumption,
  376. stdout: data.Stdout,
  377. stderr: data.Stderr,
  378. };
  379. }
  380. }
  381. }
  382.  
  383. const runners = {
  384. 4001: [new WandboxRunner("gcc-9.2.0-c", "C (GCC 9.2.0)")],
  385. 4002: [new PaizaIORunner("c", "C (C17 / Clang 10.0.0)", )],
  386. 4003: [new WandboxCppRunner("gcc-9.2.0", "C++ (GCC 9.2.0)", {options: "warning,boost-1.73.0-gcc-9.2.0,gnu++17"})],
  387. 4004: [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)", {options: "warning,boost-nothing-clang-10.0.0,c++17"})],
  388. 4006: [new PaizaIORunner("python3", "Python (3.8.2)")],
  389. 4007: [new PaizaIORunner("bash", "Bash (5.0.17)")],
  390. 4010: [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
  391. 4011: [new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)")],
  392. 4013: [new PaizaIORunner("clojure", "Clojure (1.10.1-1)")],
  393. 4017: [new PaizaIORunner("d", "D (LDC 1.23.0)")],
  394. 4020: [new PaizaIORunner("erlang", "Erlang (10.6.4)")],
  395. 4021: [new PaizaIORunner("elixir", "Elixir (1.10.4)")],
  396. 4022: [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
  397. 4023: [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
  398. 4026: [new WandboxRunner("go-1.14.1", "Go (1.14.1)")],
  399. 4027: [new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)")],
  400. 4030: [new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)")],
  401. 4032: [new PaizaIORunner("kotlin", "Kotlin (1.4.0)")],
  402. 4033: [new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)")],
  403. 4034: [new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)")],
  404. 4036: [new WandboxRunner("nim-1.0.6", "Nim (1.0.6)")],
  405. 4037: [new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)")],
  406. 4039: [new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)")],
  407. 4041: [new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)")],
  408. 4042: [new PaizaIORunner("perl", "Perl (5.30.0)")],
  409. 4044: [
  410. new PaizaIORunner("php", "PHP (7.4.10)"),
  411. new WandboxRunner("php-7.3.3", "PHP (7.3.3)"),
  412. ],
  413. 4046: [new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)")],
  414. 4047: [new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)")],
  415. 4049: [
  416. new WandboxRunner("ruby-head", "Ruby (HEAD 3.0.0dev)"),
  417. new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  418. new WandboxRunner("ruby-2.7.0-preview1", "Ruby (2.7.0-preview1)"),
  419. ],
  420. 4050: [
  421. new AtCoderRunner(4050, "Rust (1.42.0)"),
  422. new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
  423. new PaizaIORunner("rust", "Rust (1.43.0)"),
  424. ],
  425. 4051: [new PaizaIORunner("scala", "Scala (2.13.3)")],
  426. 4053: [new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)")],
  427. 4055: [new PaizaIORunner("swift", "Swift (5.2.5)")],
  428. 4056: [new CustomRunner("Text",
  429. async (sourceCode, input) => {
  430. return {
  431. status: "OK",
  432. exitCode: 0,
  433. stdout: sourceCode,
  434. };
  435. }
  436. )],
  437. 4058: [new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)")],
  438. 4061: [new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)")],
  439. 4101: [new WandboxCppRunner("gcc-9.2.0", "C++ (GCC 9.2.0)")],
  440. 4102: [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)")],
  441. };
  442.  
  443. $("#select-lang option[value]").each((_, e) => {
  444. const elem = $(e);
  445. const languageId = elem.val();
  446. if (!(languageId in runners)) runners[languageId] = [];
  447. if (runners[languageId].some(runner => runner instanceof AtCoderRunner)) return;
  448. runners[languageId].push(new AtCoderRunner(languageId, elem.text()));
  449. });
  450.  
  451. console.info("codeRunner OK");
  452.  
  453. return {
  454. run(languageId, index, sourceCode, input, supposedOutput = null, options = { trim: true, split: true, }) {
  455. if (!(languageId in runners)) {
  456. return Promise.reject("language not supported");
  457. }
  458. return runners[languageId][index].test(sourceCode, input, supposedOutput, options);
  459. },
  460.  
  461. getEnvironment(languageId) {
  462. if (!(languageId in runners)) {
  463. return Promise.reject("language not supported");
  464. }
  465. return Promise.resolve(runners[languageId].map(runner => runner.label));
  466. },
  467. };
  468. })();
  469.  
  470.  
  471. // -- bottom menu --
  472. const bottomMenu = (function () {
  473. 'use strict';
  474.  
  475. const tabs = new Set();
  476.  
  477. const bottomMenuKey = $(`<button id="bottom-menu-key" type="button" class="navbar-toggle collapsed glyphicon glyphicon-menu-down" data-toggle="collapse" data-target="#bottom-menu">`);
  478. const bottomMenuTabs = $(`<ul id="bottom-menu-tabs" class="nav nav-tabs">`);
  479. const bottomMenuContents = $(`<div id="bottom-menu-contents" class="tab-content">`);
  480.  
  481. $(() => {
  482. $(`<style>`)
  483. .text(`
  484.  
  485. #bottom-menu-wrapper {
  486. background: transparent;
  487. border: none;
  488. pointer-events: none;
  489. padding: 0;
  490. }
  491.  
  492. #bottom-menu-wrapper>.container {
  493. position: absolute;
  494. bottom: 0;
  495. width: 100%;
  496. padding: 0;
  497. }
  498.  
  499. #bottom-menu-wrapper>.container>.navbar-header {
  500. float: none;
  501. }
  502.  
  503. #bottom-menu-key {
  504. display: block;
  505. float: none;
  506. margin: 0 auto;
  507. padding: 10px 3em;
  508. border-radius: 5px 5px 0 0;
  509. background: #000;
  510. opacity: 0.5;
  511. color: #FFF;
  512. cursor: pointer;
  513. pointer-events: auto;
  514. text-align: center;
  515. }
  516.  
  517. @media screen and (max-width: 767px) {
  518. #bottom-menu-key {
  519. opacity: 0.25;
  520. }
  521. }
  522.  
  523. #bottom-menu-key.collapsed:before {
  524. content: "\\e260";
  525. }
  526.  
  527. #bottom-menu-tabs {
  528. padding: 3px 0 0 10px;
  529. cursor: n-resize;
  530. }
  531.  
  532. #bottom-menu-tabs a {
  533. pointer-events: auto;
  534. }
  535.  
  536. #bottom-menu {
  537. pointer-events: auto;
  538. background: rgba(0, 0, 0, 0.8);
  539. color: #fff;
  540. max-height: unset;
  541. }
  542.  
  543. #bottom-menu.collapse:not(.in) {
  544. display: none !important;
  545. }
  546.  
  547. #bottom-menu-tabs>li>a {
  548. background: rgba(100, 100, 100, 0.5);
  549. border: solid 1px #ccc;
  550. color: #fff;
  551. }
  552.  
  553. #bottom-menu-tabs>li>a:hover {
  554. background: rgba(150, 150, 150, 0.5);
  555. border: solid 1px #ccc;
  556. color: #333;
  557. }
  558.  
  559. #bottom-menu-tabs>li.active>a {
  560. background: #eee;
  561. border: solid 1px #ccc;
  562. color: #333;
  563. }
  564.  
  565. .bottom-menu-btn-close {
  566. font-size: 8pt;
  567. vertical-align: baseline;
  568. padding: 0 0 0 6px;
  569. margin-right: -6px;
  570. }
  571.  
  572. #bottom-menu-contents {
  573. padding: 5px 15px;
  574. max-height: 50vh;
  575. overflow-y: auto;
  576. }
  577.  
  578. #bottom-menu-contents .panel {
  579. color: #333;
  580. }
  581.  
  582. `)
  583. .appendTo("head");
  584. const bottomMenu = $(`<div id="bottom-menu" class="collapse navbar-collapse">`).append(bottomMenuTabs, bottomMenuContents);
  585. $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
  586. .append($(`<div class="container">`)
  587. .append(
  588. $(`<div class="navbar-header">`).append(bottomMenuKey),
  589. bottomMenu))
  590. .appendTo("#main-div");
  591.  
  592. let resizeStart = null;
  593. bottomMenuTabs.on({
  594. mousedown({target, pageY}) {
  595. if (!$(target).is("#bottom-menu-tabs")) return;
  596. resizeStart = {y: pageY, height: bottomMenuContents.height()};
  597. },
  598. mousemove(e) {
  599. if (!resizeStart) return;
  600. e.preventDefault();
  601. bottomMenuContents.height(resizeStart.height - (e.pageY - resizeStart.y));
  602. },
  603. });
  604. $(document).on({
  605. mouseup() {
  606. resizeStart = null;
  607. },
  608. mouseleave() {
  609. resizeStart = null;
  610. },
  611. });
  612. });
  613.  
  614. const menuController = {
  615. addTab(tabId, tabLabel, paneContent, options = {}) {
  616. console.log("addTab: %s (%s)", tabLabel, tabId, paneContent);
  617. const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
  618. .click(e => {
  619. e.preventDefault();
  620. tab.tab("show");
  621. })
  622. .append(tabLabel);
  623. const tabLi = $(`<li>`).append(tab).appendTo(bottomMenuTabs);
  624. const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo(bottomMenuContents);
  625. console.dirxml(bottomMenuContents);
  626. const controller = {
  627. close() {
  628. tabLi.remove();
  629. pane.remove();
  630. tabs.delete(tab);
  631. if (tabLi.hasClass("active") && tabs.size > 0) {
  632. tabs.values().next().value.tab("show");
  633. }
  634. },
  635.  
  636. show() {
  637. menuController.show();
  638. tab.tab("show");
  639. }
  640. };
  641. tabs.add(tab);
  642. if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
  643. if (options.active || tabs.size == 1) pane.ready(() => tab.tab("show"));
  644. return controller;
  645. },
  646.  
  647. show() {
  648. if (bottomMenuKey.hasClass("collapsed")) bottomMenuKey.click();
  649. },
  650.  
  651. toggle() {
  652. bottomMenuKey.click();
  653. },
  654. };
  655.  
  656. console.info("bottomMenu OK");
  657.  
  658. return menuController;
  659. })();
  660.  
  661. $(() => {
  662. // returns [{input, output, anchor}]
  663. function getTestCases() {
  664. const selectors = [
  665. "#task-statement p+pre.literal-block",
  666. "#task-statement pre.source-code-for-copy",
  667. "#task-statement .lang>*:nth-child(1) .div-btn-copy+pre",
  668. "#task-statement .div-btn-copy+pre",
  669. "#task-statement>.part>h3+section>pre",
  670. "#task-statement pre",
  671. ];
  672.  
  673. for (const selector of selectors) {
  674. const e = $(selector);
  675. if (e.length == 0) continue;
  676. const testcases = [];
  677. for (let i = 0; i < e.length; i += 2) {
  678. testcases.push({
  679. input: (e[i]||{}).textContent,
  680. output: (e[i+1]||{}).textContent,
  681. anchor: $(e[i]).closest(".part,.section").find("h3"),
  682. });
  683. }
  684. return testcases;
  685. }
  686.  
  687. return [];
  688. }
  689.  
  690. async function runTest(title, input, output = null) {
  691. const uid = Date.now().toString();
  692. title = title ? "Result " + title : "Result";
  693. const content = $(`<div class="container">`)
  694. .html(`
  695. <div class="row"><div class="form-group">
  696. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
  697. <div class="col-sm-8">
  698. <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  699. </div>
  700. </div></div>
  701. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  702. <div class="panel panel-default"><table class="table table-bordered">
  703. <tr id="atcoder-easy-test-${uid}-row-exit-code">
  704. <th class="text-center">Exit Code</th>
  705. <td id="atcoder-easy-test-${uid}-exit-code" class="text-right"></td>
  706. </tr>
  707. <tr id="atcoder-easy-test-${uid}-row-exec-time">
  708. <th class="text-center">Exec Time</th>
  709. <td id="atcoder-easy-test-${uid}-exec-time" class="text-right"></td>
  710. </tr>
  711. <tr id="atcoder-easy-test-${uid}-row-memory">
  712. <th class="text-center">Memory</th>
  713. <td id="atcoder-easy-test-${uid}-memory" class="text-right"></td>
  714. </tr>
  715. </table></div>
  716. </div></div>
  717. <div class="row"><div class="form-group">
  718. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
  719. <div class="col-sm-8">
  720. <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  721. </div>
  722. </div></div>
  723. <div class="row"><div class="form-group">
  724. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stderr">Standard Error</label>
  725. <div class="col-sm-8">
  726. <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  727. </div>
  728. </div></div>
  729. `);
  730. const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  731. $(`#atcoder-easy-test-${uid}-stdin`).val(input);
  732.  
  733. const options = { trim: true, split: true, };
  734. if ($("#atcoder-easy-test-allowable-error-check").prop("checked")) {
  735. options.allowableError = parseFloat($("#atcoder-easy-test-allowable-error").val());
  736. }
  737.  
  738. const result = await codeRunner.run($("#select-lang>select").val(), +$("#atcoder-easy-test-language").val(), window.getSourceCode(), input, output, options);
  739.  
  740. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0).toggleClass("bg-success", result.exitCode == 0);
  741. $(`#atcoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  742. if ("execTime" in result) $(`#atcoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  743. if ("memory" in result) $(`#atcoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  744. $(`#atcoder-easy-test-${uid}-stdout`).val(result.stdout);
  745. $(`#atcoder-easy-test-${uid}-stderr`).val(result.stderr);
  746.  
  747. result.uid = uid;
  748. result.tab = tab;
  749. return result;
  750. }
  751.  
  752. console.log("bottomMenu", bottomMenu);
  753.  
  754. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
  755. .html(`
  756. <small style="position: absolute; bottom: 0; right: 0;">AtCoder Easy Test v${VERSION}</small>
  757. <div class="row">
  758. <div class="col-12 col-md-10">
  759. <div class="form-group">
  760. <label class="control-label col-sm-2">Test Environment</label>
  761. <div class="col-sm-8">
  762. <select class="form-control" id="atcoder-easy-test-language"></select>
  763. <!--input id="atcoder-easy-test-language" type="text" class="form-control" readonly-->
  764. </div>
  765. </div>
  766. </div>
  767. </div>
  768. <div class="row">
  769. <div class="col-12 col-md-10">
  770. <div class="form-group">
  771. <label class="control-label col-sm-2" for="atcoder-easy-test-allowable-error-check">Allowable Error</label>
  772. <div class="col-sm-8">
  773. <div class="input-group">
  774. <span class="input-group-addon">
  775. <input id="atcoder-easy-test-allowable-error-check" type="checkbox" checked>
  776. </span>
  777. <input id="atcoder-easy-test-allowable-error" type="text" class="form-control" value="1e-6">
  778. </div>
  779. </div>
  780. </div>
  781. </div>
  782. </div>
  783. <div class="row">
  784. <div class="col-12 col-md-10">
  785. <div class="form-group">
  786. <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
  787. <div class="col-sm-8">
  788. <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  789. </div>
  790. </div>
  791. </div>
  792. <div class="col-12 col-md-4">
  793. <label class="control-label col-sm-2"></label>
  794. <div class="form-group">
  795. <div class="col-sm-8">
  796. <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
  797. </div>
  798. </div>
  799. </div>
  800. </div>
  801. <style>
  802. #atcoder-easy-test-language {
  803. border: none;
  804. background: transparent;
  805. font: inherit;
  806. color: #fff;
  807. }
  808. #atcoder-easy-test-language option {
  809. border: none;
  810. color: #333;
  811. font: inherit;
  812. }
  813. </style>
  814. `).ready(() => {
  815. $("#atcoder-easy-test-run").click(() => runTest("", $("#atcoder-easy-test-input").val()));
  816. $("#select-lang>select").on("change", () => setLanguage());
  817. $("#atcoder-easy-test-allowable-error").attr("disabled", this.checked);
  818. $("#atcoder-easy-test-allowable-error-check").on("change", function () {
  819. $("#atcoder-easy-test-allowable-error").attr("disabled", !this.checked);
  820. });
  821.  
  822. function setLanguage() {
  823. const languageId = $("#select-lang>select").val();
  824. codeRunner.getEnvironment(languageId).then(labels => {
  825. console.log(`language: ${labels[0]} (${languageId})`);
  826. $("#atcoder-easy-test-language").css("color", "#fff").empty().append(labels.map((label, index) => $(`<option>`).val(index).text(label)));
  827. $("#atcoder-easy-test-run").removeClass("disabled");
  828. $("#atcoder-easy-test-btn-test-all").attr("disabled", false);
  829. }, error => {
  830. console.log(`language: ? (${languageId})`);
  831. $("#atcoder-easy-test-language").css("color", "#f55").empty().append($(`<option>`).text(error));
  832. $("#atcoder-easy-test-run").addClass("disabled");
  833. $("#atcoder-easy-test-btn-test-all").attr("disabled", true);
  834. });
  835. }
  836.  
  837. setLanguage();
  838. }), { active: true });
  839.  
  840. const testfuncs = [];
  841. const runButtons = [];
  842.  
  843. const testcases = getTestCases();
  844. for (const {input, output, anchor} of testcases) {
  845. const testfunc = async () => {
  846. const title = anchor[0].childNodes[0].data;
  847. const result = await runTest(title, input, output);
  848. if (result.status == "OK" || result.status == "AC") {
  849. $(`#atcoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  850. }
  851. return result;
  852. };
  853. testfuncs.push(testfunc);
  854.  
  855. const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
  856. .text("Run")
  857. .click(async () => {
  858. await testfunc();
  859. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  860. });
  861. anchor.append(runButton);
  862. runButtons.push(runButton);
  863. }
  864.  
  865. const testAllResultRow = $(`<div class="row">`);
  866. const testAllButton = $(`<a id="atcoder-easy-test-btn-test-all" class="btn btn-default btn-sm" style="margin-left: 5px">`)
  867. .text("Test All Samples")
  868. .attr({
  869. title: "Alt+Enter",
  870. "data-toggle": "tooltip",
  871. })
  872. .click(async () => {
  873. if (testAllButton.attr("disabled")) throw new Error("Button is disabled");
  874. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  875. const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
  876. let finished = 0;
  877. const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  878. .append($(`<span aria-hidden="true">`).text("\xd7"));
  879. const resultAlert = $(`<div class="alert alert-dismissible">`)
  880. .append(closeButton)
  881. .append($(`<div class="progress">`).append(progress))
  882. .append(...statuses)
  883. .appendTo(testAllResultRow);
  884. const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
  885. const result = await testfunc();
  886. finished++;
  887. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  888. statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
  889. return result;
  890. }));
  891. if (results.every(({status}) => status == "AC")) {
  892. resultAlert.addClass("alert-success");
  893. } else {
  894. resultAlert.addClass("alert-warning");
  895. }
  896. closeButton.click(() => {
  897. for (const {tab} of results) {
  898. tab.close();
  899. }
  900. });
  901. });
  902. $("#submit").after(testAllButton).closest("form").append(testAllResultRow);
  903. $(document).on("keydown", e => {
  904. if (e.altKey) {
  905. switch (e.key) {
  906. case "Enter":
  907. testAllButton.click();
  908. break;
  909. case "Escape":
  910. bottomMenu.toggle();
  911. break;
  912. }
  913. }
  914. });
  915.  
  916. console.info("view OK");
  917. });
  918.  
  919. })();