AtCoder Easy Test

Make testing sample cases easy

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

  1. // ==UserScript==
  2. // @name AtCoder Easy Test
  3. // @namespace http://atcoder.jp/
  4. // @version 0.1.4
  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. if (res.compiler_error) {
  243. result.status = "CE";
  244. result.stdout = res.compiler_output;
  245. result.stderr = res.compiler_error;
  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. const wandboxJavaRunner = new WandboxRunner("openjdk-jdk-11+28", "Java (openjdk-11+28)");
  326. wandboxJavaRunner.run = function (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. const runners = {
  345. 4001: new WandboxRunner("gcc-9.2.0-c", "C (GCC 9.2.0)"),
  346. 4002: new PaizaIORunner("c", "C (C17 / Clang 10.0.0)"),
  347. 4003: new WandboxRunner("gcc-9.2.0", "C++ (GCC 9.2.0)"),
  348. 4004: new PaizaIORunner("cpp", "C++ (C17++ / Clang 10.0.0)"),
  349. 4005: wandboxJavaRunner,
  350. 4006: new PaizaIORunner("python3", "Python (3.8.2)"),
  351. 4007: new PaizaIORunner("bash", "Bash (5.0.17)"),
  352. 4010: new PaizaIORunner("csharp", "C# (Mono-mcs 6.8.0.105)"),
  353. 4011: new PaizaIORunner("csharp", "C# (Mono-mcs 6.8.0.105)"),
  354. 4012: new PaizaIORunner("csharp", "C# (Mono-mcs 6.8.0.105)"),
  355. 4013: new PaizaIORunner("clojure", "Clojure (1.10.1-1)"),
  356. 4015: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  357. 4016: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  358. 4017: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  359. 4020: new PaizaIORunner("erlang", "Erlang (10.6.4)"),
  360. 4021: new PaizaIORunner("elixir", "Elixir (1.10.4)"),
  361. 4022: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  362. 4023: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  363. 4026: new PaizaIORunner("go", "Go (1.15)"),
  364. 4027: new PaizaIORunner("haskell", "Haskell (GHC 8.6.5)"),
  365. 4030: new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)"),
  366. 4032: new PaizaIORunner("kotlin", "Kotlin (1.4.0)"),
  367. 4033: new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)"),
  368. 4034: new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)"),
  369. 4035: new PaizaIORunner("bash", "Bash (5.0.17)"),
  370. 4036: new WandboxRunner("nim-1.0.6", "Nim (1.0.6)"),
  371. 4037: new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)"),
  372. 4036: new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)"),
  373. 4041: new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)"),
  374. 4042: new PaizaIORunner("perl", "Perl (5.30.0)"),
  375. 4043: new PaizaIORunner("perl", "Perl (5.30.0)"),
  376. 4044: new PaizaIORunner("php", "PHP (7.4.10)"),
  377. 4046: new PaizaIORunner("pypy-head", "PyPy2 (7.3.4-alpha0)"),
  378. 4047: new PaizaIORunner("pypy-7.2.0-3", "PyPy3 (7.2.0)"),
  379. 4049: new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  380. 4050: new PaizaIORunner("rust", "Rust (1.43.0)"),
  381. 4051: new PaizaIORunner("scala", "Scala (2.13.3)"),
  382. 4052: new PaizaIORunner("java", "Java (OpenJDK 15)"),
  383. 4053: new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)"),
  384. 4055: new PaizaIORunner("swift", "Swift (5.2.5)"),
  385. 4056: {
  386. label: "Text (JavaScript)",
  387. name: "text",
  388. async run(sourceCode, input) {
  389. return {
  390. status: "OK",
  391. exitCode: 0,
  392. stdout: sourceCode,
  393. };
  394. },
  395. },
  396. 4057: new WandboxRunner("typescript-3.8.3", "TypeScript (3.8.3)"),
  397. 4058: new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)"),
  398. 4060: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  399. 4061: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  400. 4067: new WandboxRunner("vim-head", "Vim (8.2.1975)"),
  401. };
  402.  
  403. return {
  404. run(languageId, sourceCode, input) {
  405. if (!(languageId in runners)) {
  406. return Promise.reject("language not supported");
  407. }
  408. return runners[languageId].run(sourceCode, input);
  409. },
  410.  
  411. getEnvironment(languageId) {
  412. if (!(languageId in runners)) {
  413. return Promise.reject("language not supported");
  414. }
  415. return Promise.resolve(runners[languageId].label);
  416. },
  417. };
  418. })();
  419.  
  420. (function () {
  421. function setLanguage() {
  422. const languageId = $("#select-lang>select").val();
  423. codeRunner.getEnvironment(languageId).then(label => {
  424. $("#atcoder-easy-test-language").css("color", "#fff").val(label);
  425. $("#atcoder-easy-test-run").removeClass("disabled");
  426. }, error => {
  427. $("#atcoder-easy-test-language").css("color", "#f55").val(error);
  428. $("#atcoder-easy-test-run").addClass("disabled");
  429. });
  430. }
  431. setLanguage();
  432.  
  433. async function runTest(input, title = "") {
  434. const uid = Date.now().toString();
  435. title = title ? "Result " + title : "Result";
  436. const content = $(`<div class="container">`)
  437. .html(`
  438. <div class="row"><div class="form-group">
  439. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
  440. <div class="col-sm-8">
  441. <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  442. </div>
  443. </div></div>
  444. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  445. <div class="panel panel-default"><table class="table table-bordered">
  446. <tr id="atcoder-easy-test-${uid}-row-exit-code">
  447. <th class="text-center">Exit Code</th>
  448. <td id="atcoder-easy-test-${uid}-exit-code" class="text-right"></td>
  449. </tr>
  450. <tr id="atcoder-easy-test-${uid}-row-exec-time">
  451. <th class="text-center">Exec Time</th>
  452. <td id="atcoder-easy-test-${uid}-exec-time" class="text-right"></td>
  453. </tr>
  454. <tr id="atcoder-easy-test-${uid}-row-memory">
  455. <th class="text-center">Memory</th>
  456. <td id="atcoder-easy-test-${uid}-memory" class="text-right"></td>
  457. </tr>
  458. </table></div>
  459. </div></div>
  460. <div class="row"><div class="form-group">
  461. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
  462. <div class="col-sm-8">
  463. <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  464. </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}-stderr">Standard Error</label>
  468. <div class="col-sm-8">
  469. <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  470. </div>
  471. </div></div>
  472. `);
  473. const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  474. $(`#atcoder-easy-test-${uid}-stdin`).val(input);
  475.  
  476. const result = await codeRunner.run($("#select-lang>select").val(), getSourceCode(), input);
  477.  
  478. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0).toggleClass("bg-success", result.exitCode == 0);
  479. $(`#atcoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  480. if ("execTime" in result) $(`#atcoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  481. if ("memory" in result) $(`#atcoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  482. $(`#atcoder-easy-test-${uid}-stdout`).val(result.stdout);
  483. $(`#atcoder-easy-test-${uid}-stderr`).val(result.stderr);
  484.  
  485. result.uid = uid;
  486. result.tab = tab;
  487. return result;
  488. }
  489.  
  490. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
  491. .html(`
  492. <div class="row">
  493. <div class="col-12 col-md-10">
  494. <div class="form-group">
  495. <label class="control-label col-sm-2">Test Environment</label>
  496. <div class="col-sm-8">
  497. <input id="atcoder-easy-test-language" class="form-control" readonly>
  498. </div>
  499. </div>
  500. </div>
  501. </div>
  502. <div class="row">
  503. <div class="col-12 col-md-10">
  504. <div class="form-group">
  505. <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
  506. <div class="col-sm-8">
  507. <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  508. </div>
  509. </div>
  510. </div>
  511. <div class="col-12 col-md-4">
  512. <label class="control-label col-sm-2"></label>
  513. <div class="form-group">
  514. <div class="col-sm-8">
  515. <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
  516. </div>
  517. </div>
  518. </div>
  519. </div>
  520. `), { active: true });
  521. $("#atcoder-easy-test-run").click(() => runTest($("#atcoder-easy-test-input").val()));
  522. $("#select-lang>select").on("change", () => setLanguage());
  523.  
  524. const testfuncs = [];
  525.  
  526. const testcases = $(".lang>span:nth-child(1) .div-btn-copy+pre[id]").toArray();
  527. for (let i = 0; i < testcases.length; i += 2) {
  528. const input = $(testcases[i]), output = $(testcases[i+1]);
  529. const testfunc = async () => {
  530. const title = input.closest(".part").find("h3")[0].childNodes[0].data;
  531. const result = await runTest(input.text(), title);
  532. if (result.status == "OK") {
  533. if (result.stdout.trim() == output.text().trim()) {
  534. $(`#atcoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  535. result.status = "AC";
  536. } else {
  537. result.status = "WA";
  538. }
  539. }
  540. return result;
  541. };
  542. testfuncs.push(testfunc);
  543.  
  544. const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
  545. .text("Run")
  546. .click(async () => {
  547. await testfunc();
  548. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  549. });
  550. input.closest(".part").find(".btn-copy").eq(0).after(runButton);
  551. }
  552.  
  553. const testAllResultRow = $(`<div class="row">`);
  554. const testAllButton = $(`<a class="btn btn-default btn-sm" style="margin-left: 5px">`)
  555. .text("Test All Samples")
  556. .click(async () => {
  557. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  558. const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
  559. let finished = 0;
  560. const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  561. .append($(`<span aria-hidden="true">`).text("\xd7"));
  562. const resultAlert = $(`<div class="alert alert-dismissible">`)
  563. .append(closeButton)
  564. .append($(`<div class="progress">`).append(progress))
  565. .append(...statuses)
  566. .appendTo(testAllResultRow);
  567. const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
  568. const result = await testfunc();
  569. finished++;
  570. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  571. statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
  572. return result;
  573. }));
  574. if (results.every(({status}) => status == "AC")) {
  575. resultAlert.addClass("alert-success");
  576. } else {
  577. resultAlert.addClass("alert-warning");
  578. }
  579. closeButton.click(() => {
  580. for (const {tab} of results) {
  581. tab.close();
  582. }
  583. });
  584. });
  585. $("#submit").after(testAllButton).closest("form").append(testAllResultRow);
  586. })();