AtCoder Easy Test

Make testing sample cases easy

Verze ze dne 17. 08. 2021. Zobrazit nejnovější verzi.

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