AtCoder Easy Test

Make testing sample cases easy

Pada tanggal 13 November 2020. Lihat %(latest_version_link).

  1. // ==UserScript==
  2. // @name AtCoder Easy Test
  3. // @namespace http://atcoder.jp/
  4. // @version 0.1.5
  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.  
  15. // This scripts consists of three modules:
  16. // * bottom menu
  17. // * code runner
  18. // * view
  19.  
  20. // -- bottom menu --
  21. if (!window.bottomMenu) { var bottomMenu = (function () {
  22. 'use strict';
  23.  
  24. const tabs = new Set();
  25.  
  26. $(() => {
  27. $(`<style>`)
  28. .text(`
  29.  
  30. #bottom-menu-wrapper {
  31. background: transparent;
  32. border: none;
  33. pointer-events: none;
  34. padding: 0;
  35. }
  36.  
  37. #bottom-menu-wrapper>.container {
  38. position: absolute;
  39. bottom: 0;
  40. width: 100%;
  41. padding: 0;
  42. }
  43.  
  44. #bottom-menu-wrapper>.container>.navbar-header {
  45. float: none;
  46. }
  47.  
  48. #bottom-menu-key {
  49. display: block;
  50. float: none;
  51. margin: 0 auto;
  52. padding: 10px 3em;
  53. border-radius: 5px 5px 0 0;
  54. background: #000;
  55. opacity: 0.85;
  56. color: #FFF;
  57. cursor: pointer;
  58. pointer-events: auto;
  59. text-align: center;
  60. }
  61.  
  62. #bottom-menu-key.collapsed:before {
  63. content: "\\e260";
  64. }
  65.  
  66. #bottom-menu-tabs {
  67. padding: 3px 0 0 10px;
  68. }
  69.  
  70. #bottom-menu-tabs a {
  71. pointer-events: auto;
  72. }
  73.  
  74. #bottom-menu {
  75. pointer-events: auto;
  76. background: rgba(0, 0, 0, 0.8);
  77. color: #fff;
  78. max-height: unset;
  79. }
  80.  
  81. #bottom-menu.collapse:not(.in) {
  82. display: none !important;
  83. }
  84.  
  85. #bottom-menu-tabs>li>a {
  86. background: rgba(100, 100, 100, 0.5);
  87. border: solid 1px #ccc;
  88. color: #fff;
  89. }
  90.  
  91. #bottom-menu-tabs>li>a:hover {
  92. background: rgba(150, 150, 150, 0.5);
  93. border: solid 1px #ccc;
  94. color: #333;
  95. }
  96.  
  97. #bottom-menu-tabs>li.active>a {
  98. background: #eee;
  99. border: solid 1px #ccc;
  100. color: #333;
  101. }
  102.  
  103. .bottom-menu-btn-close {
  104. font-size: 8pt;
  105. vertical-align: baseline;
  106. padding: 0 0 0 6px;
  107. margin-right: -6px;
  108. }
  109.  
  110. #bottom-menu-contents {
  111. padding: 5px 15px;
  112. max-height: 50vh;
  113. overflow-y: auto;
  114. }
  115.  
  116. #bottom-menu-contents .panel {
  117. color: #333;
  118. }
  119.  
  120.  
  121.  
  122. #atcoder-easy-test-language {
  123. border: none;
  124. background: transparent;
  125. font: inherit;
  126. color: #fff;
  127. }
  128.  
  129. `)
  130. .appendTo("head");
  131. $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
  132. .html(`
  133. <div class="container">
  134. <div class="navbar-header">
  135. <button id="bottom-menu-key" type="button" class="navbar-toggle collapsed glyphicon glyphicon-menu-down" data-toggle="collapse" data-target="#bottom-menu">
  136. </button>
  137. </div>
  138. <div id="bottom-menu" class="collapse navbar-collapse">
  139. <ul id="bottom-menu-tabs" class="nav nav-tabs">
  140. </ul>
  141. <div id="bottom-menu-contents" class="tab-content">
  142. </div>
  143. </div>
  144. </div>
  145. `)
  146. .appendTo("#main-div");
  147. });
  148.  
  149. const menuController = {
  150. addTab(tabId, tabLabel, paneContent, options = {}) {
  151. const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
  152. .click(e => {
  153. e.preventDefault();
  154. tab.tab("show");
  155. })
  156. .append(tabLabel);
  157. const tabLi = $(`<li>`).append(tab).appendTo("#bottom-menu-tabs");
  158. const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo("#bottom-menu-contents");
  159. const controller = {
  160. close() {
  161. tabLi.remove();
  162. pane.remove();
  163. tabs.delete(tab);
  164. if (tabLi.hasClass("active") && tabs.size > 0) {
  165. tabs.values().next().value.tab("show");
  166. }
  167. },
  168.  
  169. show() {
  170. menuController.show();
  171. tab.tab("show");
  172. }
  173. };
  174. tabs.add(tab);
  175. if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
  176. if (options.active || tabs.size == 1) tab.tab("show");
  177. return controller;
  178. },
  179.  
  180. show() {
  181. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  182. },
  183. };
  184.  
  185. return menuController;
  186. })(); }
  187.  
  188. // -- code runner --
  189. var codeRunner = (function() {
  190. 'use strict';
  191.  
  192. function buildParams(data) {
  193. return Object.entries(data).map(([key, value]) =>
  194. encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  195. }
  196.  
  197. class WandboxRunner {
  198. constructor(name, label, options = {}) {
  199. this.name = name;
  200. this.label = label + " [Wandbox]";
  201. }
  202.  
  203. run(sourceCode, input) {
  204. return this.request(Object.assign(JSON.stringify({
  205. compiler: this.name,
  206. code: sourceCode,
  207. stdin: input,
  208. }), this.options));
  209. }
  210.  
  211. async request(body) {
  212. const startTime = Date.now();
  213. let res;
  214. try {
  215. res = await fetch("https://wandbox.org/api/compile.json", {
  216. method: "POST",
  217. mode: "cors",
  218. headers: {
  219. "Content-Type": "application/json",
  220. },
  221. body,
  222. }).then(r => r.json());
  223. } catch (error) {
  224. return {
  225. status: "IE",
  226. stderr: error,
  227. };
  228. }
  229. const endTime = Date.now();
  230.  
  231. const result = {
  232. status: "OK",
  233. exitCode: res.status,
  234. execTime: endTime - startTime,
  235. stdout: res.program_output,
  236. stderr: res.program_error,
  237. };
  238. if (res.status != 0) {
  239. if (res.signal) {
  240. result.exitCode += " (" + res.signal + ")";
  241. }
  242. result.stdout = (res.compiler_output || "") + (result.stdout || "");
  243. result.stderr = (res.compiler_error || "") + (result.stderr || "");
  244. if (res.compiler_output || res.compiler_error) {
  245. result.status = "CE";
  246. } else {
  247. result.status = "RE";
  248. }
  249. }
  250.  
  251. return result;
  252. }
  253. }
  254.  
  255. class PaizaIORunner {
  256. constructor(name, label) {
  257. this.name = name;
  258. this.label = label + "[PaizaIO]";
  259. }
  260.  
  261. async run(sourceCode, input) {
  262. let id, status, error;
  263. try {
  264. const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
  265. source_code: sourceCode,
  266. language: this.name,
  267. input,
  268. longpoll: true,
  269. longpoll_timeout: 10,
  270. api_key: "guest",
  271. }), {
  272. method: "POST",
  273. mode: "cors",
  274. }).then(r => r.json());
  275. id = res.id;
  276. status = res.status;
  277. error = res.error;
  278. } catch (error) {
  279. return {
  280. status: "IE",
  281. stderr: error,
  282. };
  283. }
  284.  
  285. while (status == "running") {
  286. const res = await (await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
  287. id,
  288. api_key: "guest",
  289. }), {
  290. mode: "cors",
  291. })).json();
  292. status = res.status;
  293. error = res.error;
  294. }
  295.  
  296. const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
  297. id,
  298. api_key: "guest",
  299. }), {
  300. mode: "cors",
  301. }).then(r => r.json());
  302.  
  303. const result = {
  304. exitCode: res.exit_code,
  305. execTime: +res.time * 1e3,
  306. memory: +res.memory * 1e-3,
  307. };
  308.  
  309. if (res.build_result == "failure") {
  310. result.status = "CE";
  311. result.exitCode = res.build_exit_code;
  312. result.stdout = res.build_stdout;
  313. result.stderr = res.build_stderr;
  314. } else {
  315. result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
  316. result.exitCode = res.exit_code;
  317. result.stdout = res.stdout;
  318. result.stderr = res.stderr;
  319. }
  320.  
  321. return result;
  322. }
  323. }
  324.  
  325. class WandboxJavaRunner extends WandboxRunner {
  326. run(sourceCode, input) {
  327. return this.request(JSON.stringify({
  328. compiler: this.name,
  329. code: `
  330. public class prog {
  331. public static void main(String[] args) {
  332. Main.main(args);
  333. }
  334. }
  335. `,
  336. codes: [{
  337. file: "Main.java",
  338. code: sourceCode,
  339. }],
  340. stdin: input,
  341. }));
  342. }
  343. }
  344.  
  345. const runners = {
  346. 4001: new WandboxRunner("gcc-9.2.0-c", "C (GCC 9.2.0)"),
  347. 4002: new PaizaIORunner("c", "C (C17 / Clang 10.0.0)"),
  348. 4003: new WandboxRunner("gcc-9.2.0", "C++ (GCC 9.2.0)"),
  349. 4004: new WandboxRunner("clang-10.0.0", "C++ (Clang 10.0.0)"),
  350. 4005: new WandboxJavaRunner("openjdk-jdk-11+28", "Java (openjdk-11+28)"),
  351. 4006: new PaizaIORunner("python3", "Python (3.8.2)"),
  352. 4007: new PaizaIORunner("bash", "Bash (5.0.17)"),
  353. 4010: new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)"),
  354. 4011: new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)"),
  355. 4012: new PaizaIORunner("csharp", "C# (Mono-mcs 6.8.0.105)"),
  356. 4013: new PaizaIORunner("clojure", "Clojure (1.10.1-1)"),
  357. 4015: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  358. 4016: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  359. 4017: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  360. 4020: new PaizaIORunner("erlang", "Erlang (10.6.4)"),
  361. 4021: new PaizaIORunner("elixir", "Elixir (1.10.4)"),
  362. 4022: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  363. 4023: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  364. 4026: new WandboxRunner("go-1.14.1", "Go (1.14.1)"),
  365. 4027: new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)"),
  366. 4030: new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)"),
  367. 4032: new PaizaIORunner("kotlin", "Kotlin (1.4.0)"),
  368. 4033: new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)"),
  369. 4034: new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)"),
  370. 4035: new PaizaIORunner("bash", "Bash (5.0.17)"),
  371. 4036: new WandboxRunner("nim-1.0.6", "Nim (1.0.6)"),
  372. 4037: new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)"),
  373. 4036: new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)"),
  374. 4041: new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)"),
  375. 4042: new PaizaIORunner("perl", "Perl (5.30.0)"),
  376. 4043: new PaizaIORunner("perl", "Perl (5.30.0)"),
  377. 4044: new PaizaIORunner("php", "PHP (7.4.10)"),
  378. 4046: new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)"),
  379. 4047: new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)"),
  380. 4049: new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  381. 4050: new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
  382. 4051: new PaizaIORunner("scala", "Scala (2.13.3)"),
  383. 4052: new WandboxJavaRunner("openjdk-jdk8u121-b13", "Java (jdk8u121-b13)"),
  384. 4053: new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)"),
  385. 4055: new PaizaIORunner("swift", "Swift (5.2.5)"),
  386. 4056: {
  387. label: "Text (JavaScript)",
  388. name: "text",
  389. async run(sourceCode, input) {
  390. return {
  391. status: "OK",
  392. exitCode: 0,
  393. stdout: sourceCode,
  394. };
  395. },
  396. },
  397. 4057: new WandboxRunner("typescript-3.8.3", "TypeScript (3.8.3)", {
  398. "compiler-option-raw": `--types=node --moduleResolution=node --lib=esnext`,
  399. "runtime-option-raw": `--types=node --moduleResolution=node --lib=esnext`,
  400. }),
  401. 4058: new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)"),
  402. 4060: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  403. 4061: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  404. 4067: new WandboxRunner("vim-head", "Vim (8.2.1975)"),
  405. };
  406.  
  407. return {
  408. run(languageId, sourceCode, input) {
  409. if (!(languageId in runners)) {
  410. return Promise.reject("language not supported");
  411. }
  412. return runners[languageId].run(sourceCode, input);
  413. },
  414.  
  415. getEnvironment(languageId) {
  416. if (!(languageId in runners)) {
  417. return Promise.reject("language not supported");
  418. }
  419. return Promise.resolve(runners[languageId].label);
  420. },
  421. };
  422. })();
  423.  
  424. (function () {
  425. function setLanguage() {
  426. const languageId = $("#select-lang>select").val();
  427. codeRunner.getEnvironment(languageId).then(label => {
  428. $("#atcoder-easy-test-language").css("color", "#fff").val(label);
  429. $("#atcoder-easy-test-run").removeClass("disabled");
  430. $("#atcoder-easy-test-btn-test-all").attr("disabled", false);
  431. }, error => {
  432. $("#atcoder-easy-test-language").css("color", "#f55").val(error);
  433. $("#atcoder-easy-test-run").addClass("disabled");
  434. $("#atcoder-easy-test-btn-test-all").attr("disabled", true);
  435. });
  436. }
  437. setLanguage();
  438.  
  439. async function runTest(input, title = "") {
  440. const uid = Date.now().toString();
  441. title = title ? "Result " + title : "Result";
  442. const content = $(`<div class="container">`)
  443. .html(`
  444. <div class="row"><div class="form-group">
  445. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
  446. <div class="col-sm-8">
  447. <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  448. </div>
  449. </div></div>
  450. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  451. <div class="panel panel-default"><table class="table table-bordered">
  452. <tr id="atcoder-easy-test-${uid}-row-exit-code">
  453. <th class="text-center">Exit Code</th>
  454. <td id="atcoder-easy-test-${uid}-exit-code" class="text-right"></td>
  455. </tr>
  456. <tr id="atcoder-easy-test-${uid}-row-exec-time">
  457. <th class="text-center">Exec Time</th>
  458. <td id="atcoder-easy-test-${uid}-exec-time" class="text-right"></td>
  459. </tr>
  460. <tr id="atcoder-easy-test-${uid}-row-memory">
  461. <th class="text-center">Memory</th>
  462. <td id="atcoder-easy-test-${uid}-memory" class="text-right"></td>
  463. </tr>
  464. </table></div>
  465. </div></div>
  466. <div class="row"><div class="form-group">
  467. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
  468. <div class="col-sm-8">
  469. <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  470. </div>
  471. </div></div>
  472. <div class="row"><div class="form-group">
  473. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stderr">Standard Error</label>
  474. <div class="col-sm-8">
  475. <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  476. </div>
  477. </div></div>
  478. `);
  479. const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  480. $(`#atcoder-easy-test-${uid}-stdin`).val(input);
  481.  
  482. const result = await codeRunner.run($("#select-lang>select").val(), getSourceCode(), input);
  483.  
  484. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0).toggleClass("bg-success", result.exitCode == 0);
  485. $(`#atcoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  486. if ("execTime" in result) $(`#atcoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  487. if ("memory" in result) $(`#atcoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  488. $(`#atcoder-easy-test-${uid}-stdout`).val(result.stdout);
  489. $(`#atcoder-easy-test-${uid}-stderr`).val(result.stderr);
  490.  
  491. result.uid = uid;
  492. result.tab = tab;
  493. return result;
  494. }
  495.  
  496. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
  497. .html(`
  498. <div class="row">
  499. <div class="col-12 col-md-10">
  500. <div class="form-group">
  501. <label class="control-label col-sm-2">Test Environment</label>
  502. <div class="col-sm-8">
  503. <input id="atcoder-easy-test-language" class="form-control" readonly>
  504. </div>
  505. </div>
  506. </div>
  507. </div>
  508. <div class="row">
  509. <div class="col-12 col-md-10">
  510. <div class="form-group">
  511. <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
  512. <div class="col-sm-8">
  513. <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  514. </div>
  515. </div>
  516. </div>
  517. <div class="col-12 col-md-4">
  518. <label class="control-label col-sm-2"></label>
  519. <div class="form-group">
  520. <div class="col-sm-8">
  521. <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
  522. </div>
  523. </div>
  524. </div>
  525. </div>
  526. `), { active: true });
  527. $("#atcoder-easy-test-run").click(() => runTest($("#atcoder-easy-test-input").val()));
  528. $("#select-lang>select").on("change", () => setLanguage());
  529.  
  530. const testfuncs = [];
  531.  
  532. const testcases = $(".lang>span:nth-child(1) .div-btn-copy+pre[id]").toArray();
  533. for (let i = 0; i < testcases.length; i += 2) {
  534. const input = $(testcases[i]), output = $(testcases[i+1]);
  535. const testfunc = async () => {
  536. const title = input.closest(".part").find("h3")[0].childNodes[0].data;
  537. const result = await runTest(input.text(), title);
  538. if (result.status == "OK") {
  539. if (result.stdout.trim() == output.text().trim()) {
  540. $(`#atcoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  541. result.status = "AC";
  542. } else {
  543. result.status = "WA";
  544. }
  545. }
  546. return result;
  547. };
  548. testfuncs.push(testfunc);
  549.  
  550. const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
  551. .text("Run")
  552. .click(async () => {
  553. await testfunc();
  554. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  555. });
  556. input.closest(".part").find(".btn-copy").eq(0).after(runButton);
  557. }
  558.  
  559. const testAllResultRow = $(`<div class="row">`);
  560. const testAllButton = $(`<a id="atcoder-easy-test-btn-test-all" class="btn btn-default btn-sm" style="margin-left: 5px">`)
  561. .text("Test All Samples")
  562. .click(async () => {
  563. if (testAllButton.attr("disabled")) throw new Error("Button is disabled");
  564. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  565. const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
  566. let finished = 0;
  567. const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  568. .append($(`<span aria-hidden="true">`).text("\xd7"));
  569. const resultAlert = $(`<div class="alert alert-dismissible">`)
  570. .append(closeButton)
  571. .append($(`<div class="progress">`).append(progress))
  572. .append(...statuses)
  573. .appendTo(testAllResultRow);
  574. const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
  575. const result = await testfunc();
  576. finished++;
  577. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  578. statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
  579. return result;
  580. }));
  581. if (results.every(({status}) => status == "AC")) {
  582. resultAlert.addClass("alert-success");
  583. } else {
  584. resultAlert.addClass("alert-warning");
  585. }
  586. closeButton.click(() => {
  587. for (const {tab} of results) {
  588. tab.close();
  589. }
  590. });
  591. });
  592. $("#submit").after(testAllButton).closest("form").append(testAllResultRow);
  593. })();