AtCoder Easy Test

Make testing sample cases easy

As of 2021-09-19. See the latest version.

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