AtCoder Easy Test

Make testing sample cases easy

As of 2020-11-11. See the latest version.

  1. // ==UserScript==
  2. // @name AtCoder Easy Test
  3. // @namespace http://atcoder.jp/
  4. // @version 0.1
  5. // @description Make testing sample cases easy
  6. // @author magurofly
  7. // @match https://atcoder.jp/contests/*/tasks/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // This script uses variables from page below:
  12. // * `$`
  13. // * `getSourceCode`
  14.  
  15. if (!window.bottomMenu) { var bottomMenu = (function () {
  16. 'use strict';
  17.  
  18. const tabs = new Set();
  19.  
  20. $(() => {
  21. $(`<style>`)
  22. .text(`
  23.  
  24. #bottom-menu-wrapper {
  25. background: transparent;
  26. border: none;
  27. pointer-events: none;
  28. padding: 0;
  29. }
  30.  
  31. #bottom-menu-wrapper>.container {
  32. position: absolute;
  33. bottom: 0;
  34. width: 100%;
  35. padding: 0;
  36. }
  37.  
  38. #bottom-menu-wrapper>.container>.navbar-header {
  39. float: none;
  40. }
  41.  
  42. #bottom-menu-key {
  43. display: block;
  44. float: none;
  45. margin: 0 auto;
  46. padding: 10px 3em;
  47. border-radius: 5px 5px 0 0;
  48. background: #000;
  49. opacity: 0.85;
  50. color: #FFF;
  51. cursor: pointer;
  52. pointer-events: auto;
  53. text-align: center;
  54. }
  55.  
  56. #bottom-menu-key.collapsed:before {
  57. content: "\\e260";
  58. }
  59.  
  60. #bottom-menu-tabs {
  61. padding: 3px 0 0 10px;
  62. }
  63.  
  64. #bottom-menu-tabs a {
  65. pointer-events: auto;
  66. }
  67.  
  68. #bottom-menu {
  69. pointer-events: auto;
  70. background: rgba(0, 0, 0, 0.8);
  71. color: #fff;
  72. max-height: unset;
  73. }
  74.  
  75. #bottom-menu.collapse:not(.in) {
  76. display: none !important;
  77. }
  78.  
  79. #bottom-menu-tabs>li>a {
  80. background: rgba(100, 100, 100, 0.5);
  81. border: solid 1px #ccc;
  82. color: #fff;
  83. }
  84.  
  85. #bottom-menu-tabs>li>a:hover {
  86. background: rgba(150, 150, 150, 0.5);
  87. border: solid 1px #ccc;
  88. color: #333;
  89. }
  90.  
  91. #bottom-menu-tabs>li.active>a {
  92. background: #eee;
  93. border: solid 1px #ccc;
  94. color: #333;
  95. }
  96.  
  97. .bottom-menu-btn-close {
  98. font-size: 8pt;
  99. vertical-align: baseline;
  100. padding: 0 0 0 6px;
  101. margin-right: -6px;
  102. }
  103.  
  104. #bottom-menu-contents {
  105. padding: 5px 15px;
  106. max-height: 50vh;
  107. overflow-y: auto;
  108. }
  109.  
  110. #bottom-menu-contents .panel {
  111. color: #333;
  112. }
  113.  
  114.  
  115.  
  116. #atcoder-easy-test-language {
  117. border: none;
  118. background: transparent;
  119. font: inherit;
  120. color: #fff;
  121. }
  122.  
  123. `)
  124. .appendTo("head");
  125. $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
  126. .html(`
  127. <div class="container">
  128. <div class="navbar-header">
  129. <button id="bottom-menu-key" type="button" class="navbar-toggle collapsed glyphicon glyphicon-menu-down" data-toggle="collapse" data-target="#bottom-menu">
  130. </button>
  131. </div>
  132. <div id="bottom-menu" class="collapse navbar-collapse">
  133. <ul id="bottom-menu-tabs" class="nav nav-tabs">
  134. </ul>
  135. <div id="bottom-menu-contents" class="tab-content">
  136. </div>
  137. </div>
  138. </div>
  139. `)
  140. .appendTo("#main-div");
  141. });
  142.  
  143. return {
  144. addTab(tabId, tabLabel, paneContent, options = {}) {
  145. const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
  146. .click(e => {
  147. e.preventDefault();
  148. tab.tab("show");
  149. })
  150. .append(tabLabel);
  151. const tabLi = $(`<li>`).append(tab).appendTo("#bottom-menu-tabs");
  152. const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo("#bottom-menu-contents");
  153. const controller = {
  154. close() {
  155. tabLi.remove();
  156. pane.remove();
  157. tabs.delete(tab);
  158. if (tabLi.hasClass("active") && tabs.size > 0) {
  159. tabs.values().next().value.tab("show");
  160. }
  161. }
  162. };
  163. tabs.add(tab);
  164. if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
  165. if (options.active || tabs.size == 1) tab.tab("show");
  166. return controller;
  167. }
  168. };
  169. })(); }
  170.  
  171. (function() {
  172. 'use strict';
  173.  
  174. let languageName = null;
  175.  
  176. const languageIdMap = {
  177. 4001: "c", 4002: "c",
  178. 4003: "cpp", 4004: "cpp",
  179. 4005: "java", 4052: "java",
  180. 4046: "python",
  181. 4006: "python3", 4047: "python3",
  182. 4007: "bash", 4035: "bash",
  183. 4010: "csharp", 4011: "csharp", 4012: "csharp",
  184. 4013: "clojure",
  185. 4015: "d", 4016: "d", 4017: "d",
  186. 4020: "erlang",
  187. 4021: "elixir",
  188. 4022: "fsharp", 4023: "fsharp",
  189. 4026: "go",
  190. 4027: "haskell",
  191. 4030: "javascript",
  192. 4032: "kotlin",
  193. 4037: "objective-c",
  194. 4042: "perl", 4043: "perl",
  195. 4044: "php",
  196. 4049: "ruby",
  197. 4050: "rust",
  198. 4051: "scala",
  199. 4053: "scheme",
  200. 4055: "swift",
  201. 4058: "vb",
  202. 4060: "cobol", 4061: "cobol",
  203. };
  204.  
  205. const languageLabelMap = {
  206. c: "C (C17 / Clang 10.0.0)",
  207. cpp: "C++ (C17++ / Clang 10.0.0)",
  208. java: "Java (OpenJDK 15)",
  209. python: "Python (2.7.18rc1)",
  210. python3: "Python (3.8.2)",
  211. bash: "Bash (5.0.17)",
  212. csharp: "C# (Mono-mcs 6.8.0.105)",
  213. clojure: "Clojure (1.10.1-1)",
  214. d: "D (LDC 1.23.0)",
  215. erlang: "Erlang (10.6.4)",
  216. elixir: "Elixir (1.10.4)",
  217. fsharp: "F# (Interactive 4.0)",
  218. go: "Go (1.15)",
  219. haskell: "Haskell (GHC 8.6.5)",
  220. javascript: "JavaScript (Node.js 12.18.3)",
  221. kotlin: "Kotlin (1.4.0)",
  222. "objective-c": "Objective-C (Clang 10.0.0)",
  223. perl: "Perl (5.30.0)",
  224. php: "PHP (7.4.10)",
  225. ruby: "Ruby (2.7.1)",
  226. rust: "Rust (1.43.0)",
  227. scala: "Scala (2.13.3)",
  228. scheme: "Scheme (Gauche 0.9.6)",
  229. swift: "Swift (5.2.5)",
  230. vb: "Visual Basic (.NET Core 4.0.1)",
  231. cobol: "COBOL - Free (OpenCOBOL 2.2.0)",
  232. };
  233.  
  234. function setLanguage() {
  235. const languageId = $("#select-lang>select").val();
  236. if (languageId in languageIdMap) {
  237. languageName = languageIdMap[languageId];
  238. $("#atcoder-easy-test-language").css("color", "#fff").val(languageLabelMap[languageName]);
  239. $("#atcoder-easy-test-run").removeClass("disabled");
  240. } else {
  241. $("#atcoder-easy-test-language").css("color", "#f55").val("language not supported on paiza.io");
  242. $("#atcoder-easy-test-run").addClass("disabled");
  243. }
  244. console.log(languageId);
  245. }
  246.  
  247. async function getJSON(method, url, data) {
  248. const params = Object.entries(data).map(([key, value]) =>
  249. encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  250. const response = await fetch(url + "?" + params, {
  251. method,
  252. mode: "cors",
  253. headers: {
  254. "Accept": "application/json",
  255. },
  256. });
  257. return await response.json();
  258. }
  259.  
  260. async function submitTest(input) {
  261. let {id, status, error} = await getJSON("POST", "https://api.paiza.io/runners/create", {
  262. source_code: getSourceCode(),
  263. language: languageName,
  264. input,
  265. longpoll: true,
  266. longpoll_timeout: 10,
  267. api_key: "guest",
  268. });
  269. console.log("runner id: %s: %s", id, status);
  270.  
  271. while (status != "completed") {
  272. let response = await getJSON("GET", "https://api.paiza.io/runners/get_status", {
  273. id,
  274. api_key: "guest",
  275. });
  276. console.log("%s: %s" ,id, status);
  277. status = response.status;
  278. error = response.error;
  279. }
  280.  
  281. if (error) console.error("%s: %s", id, error);
  282.  
  283. let {build_stderr, build_exit_code, build_result, stdout, stderr, exit_code, time, memory, result} = await getJSON("GET", "https://api.paiza.io/runners/get_details", {
  284. id,
  285. api_key: "guest",
  286. });
  287.  
  288. console.info("%s: %s", id, result);
  289.  
  290. if (build_exit_code != 0) {
  291. return {
  292. status: "CE",
  293. exitCode: build_exit_code,
  294. stderr: build_stderr,
  295. };
  296. }
  297.  
  298. return {
  299. status: exit_code == 0 ? "OK" : result == "timeout" ? "TLE" : "RE",
  300. exitCode: exit_code,
  301. execTime: +time * 1e3,
  302. memory: memory * 1e-3,
  303. stdout,
  304. stderr,
  305. };
  306. }
  307.  
  308. async function runTest(input, title = "") {
  309. const uid = Date.now().toString();
  310. title = title ? "Result " + title : "Result";
  311. const content = $(`<div class="container">`)
  312. .html(`
  313. <div class="row"><div class="form-group">
  314. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
  315. <div class="col-sm-8">
  316. <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  317. </div>
  318. </div></div>
  319. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  320. <div class="panel panel-default"><table class="table table-bordered">
  321. <tr id="atcoder-easy-test-${uid}-row-exit-code">
  322. <th class="text-center">Exit Code</th>
  323. <td id="atcoder-easy-test-${uid}-exit-code" class="text-right"></td>
  324. </tr>
  325. <tr id="atcoder-easy-test-${uid}-row-exec-time">
  326. <th class="text-center">Exec Time</th>
  327. <td id="atcoder-easy-test-${uid}-exec-time" class="text-right"></td>
  328. </tr>
  329. <tr id="atcoder-easy-test-${uid}-row-memory">
  330. <th class="text-center">Memory</th>
  331. <td id="atcoder-easy-test-${uid}-memory" class="text-right"></td>
  332. </tr>
  333. </table></div>
  334. </div></div>
  335. <div class="row"><div class="form-group">
  336. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
  337. <div class="col-sm-8">
  338. <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  339. </div>
  340. </div></div>
  341. <div class="row"><div class="form-group">
  342. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stderr">Standard Error</label>
  343. <div class="col-sm-8">
  344. <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  345. </div>
  346. </div></div>
  347. `);
  348. bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  349. $(`#atcoder-easy-test-${uid}-stdin`).val(input);
  350.  
  351. const result = await submitTest(input);
  352.  
  353. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0);
  354. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-success", result.exitCode == 0);
  355. $(`#atcoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  356. if ("execTime" in result) $(`#atcoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  357. if ("memory" in result) $(`#atcoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  358. $(`#atcoder-easy-test-${uid}-stdout`).val(result.stdout);
  359. $(`#atcoder-easy-test-${uid}-stderr`).val(result.stderr);
  360.  
  361. result.uid = uid;
  362. return result;
  363. }
  364.  
  365. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
  366. .html(`
  367. <div class="row">
  368. <div class="col-12 col-md-10">
  369. <div class="form-group">
  370. <label class="control-label col-sm-2">Test Environment</label>
  371. <div class="col-sm-8">
  372. <input id="atcoder-easy-test-language" class="form-control" readonly>
  373. </div>
  374. </div>
  375. </div>
  376. </div>
  377. <div class="row">
  378. <div class="col-12 col-md-10">
  379. <div class="form-group">
  380. <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
  381. <div class="col-sm-8">
  382. <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  383. </div>
  384. </div>
  385. </div>
  386. <div class="col-12 col-md-4">
  387. <label class="control-label col-sm-2"></label>
  388. <div class="form-group">
  389. <div class="col-sm-8">
  390. <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
  391. </div>
  392. </div>
  393. </div>
  394. </div>
  395. `), { active: true });
  396. $("#atcoder-easy-test-run").click(() => runTest($("#atcoder-easy-test-input").val()));
  397. $("#select-lang>select").on("change", () => setLanguage());
  398.  
  399. const testfuncs = [];
  400.  
  401. const testcases = $(".lang>span:nth-child(1) .div-btn-copy+pre[id]").toArray();
  402. for (let i = 0; i < testcases.length; i += 2) {
  403. const input = $(testcases[i]), output = $(testcases[i+1]);
  404. const testfunc = async () => {
  405. const title = input.closest(".part").find("h3")[0].childNodes[0].data;
  406. const result = await runTest(input.text(), title);
  407. if (result.status == "OK") {
  408. if (result.stdout.trim() == output.text().trim()) {
  409. $(`#atcoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  410. return "AC";
  411. } else {
  412. return "WA";
  413. }
  414. }
  415. return result.status;
  416. };
  417. testfuncs.push(testfunc);
  418.  
  419. const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
  420. .text("Run")
  421. .click(async () => {
  422. await testfunc();
  423. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  424. });
  425. input.closest(".part").find(".btn-copy").eq(0).after(runButton);
  426. }
  427.  
  428. const testAllResultRow = $(`<div class="row">`);
  429. const testAllButton = $(`<a class="btn btn-default btn-sm" style="margin-left: 5px">`)
  430. .text("Test All Samples")
  431. .click(async () => {
  432. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  433. const progress = $(`<div class="progress-bar">`).text(`0 / ${statuses.length}`);
  434. let finished = 0;
  435. const resultAlert = $(`<div class="alert alert-dismissible">`)
  436. .append($(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  437. .append($(`<span aria-hidden="true">`).text("\xd7")))
  438. .append($(`<div class="progress">`).append(progress))
  439. .append(...statuses)
  440. .appendTo(testAllResultRow);
  441. const waitee = testfuncs.map(async (testfunc, i) => {
  442. const status = await testfunc();
  443. finished++;
  444. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  445. statuses[i].toggleClass("label-success", status == "AC").toggleClass("label-warning", status != "AC").text(status);
  446. return status;
  447. });
  448. if ((await Promise.all(waitee)).every(status => status == "AC")) {
  449. resultAlert.addClass("alert-success");
  450. } else {
  451. resultAlert.addClass("alert-warning");
  452. }
  453. });
  454. $("#submit").after(testAllButton).closest("form").append(testAllResultRow);
  455. })();