diff --git a/.busted b/.busted deleted file mode 100644 index f4945a0..0000000 --- a/.busted +++ /dev/null @@ -1,13 +0,0 @@ -return { - _all = { - coverage = false, - lpath = 'lua/?.lua;lua/?/init.lua', - lua = 'nlua', - }, - default = { - verbose = true, - }, - tests = { - verbose = true, - }, -} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 731ad4f..4c1cc1f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,21 +35,6 @@ jobs: - 'pyproject.toml' - 'uv.lock' - lua-test: - name: Lua Tests (${{ matrix.neovim_version }}) - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.lua == 'true' }} - strategy: - matrix: - neovim_version: ['stable', 'nightly'] - steps: - - uses: actions/checkout@v4 - - name: Run Lua tests - uses: nvim-neorocks/nvim-busted-action@v1 - with: - nvim_version: ${{ matrix.neovim_version }} - python-test: name: Python Tests runs-on: ubuntu-latest diff --git a/README.md b/README.md index 5d6088c..a0dd3ce 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ https://github.com/user-attachments/assets/956ec4c4-5ef1-4391-abea-3a51fa771809 ## Features -- **Multi-platform support**: AtCoder, Codeforces, CSES with consistent - interface +- **Multi-platform support**: AtCoder, CodeChef, Codeforces, and CSES - **Automatic problem setup**: Scrape test cases and metadata in seconds - **Dual view modes**: Lightweight I/O view for quick feedback, full panel for detailed analysis diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 5b3b584..78f321f 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -139,6 +139,10 @@ M.defaults = { enabled_languages = { 'cpp', 'python' }, default_language = 'cpp', }, + codechef = { + enabled_languages = { 'cpp', 'python' }, + default_language = 'cpp', + }, cses = { enabled_languages = { 'cpp', 'python' }, default_language = 'cpp', diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 9d1f0cc..7bdaa16 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -1,10 +1,11 @@ local M = {} -M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' } +M.PLATFORMS = { 'atcoder', 'codechef', 'codeforces', 'cses' } M.ACTIONS = { 'run', 'panel', 'next', 'prev', 'pick', 'cache', 'interact', 'edit' } M.PLATFORM_DISPLAY_NAMES = { atcoder = 'AtCoder', + codechef = 'CodeChef', codeforces = 'CodeForces', cses = 'CSES', } diff --git a/pyproject.toml b/pyproject.toml index b114d87..1e09ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dev = [ "pytest-mock>=3.12.0", "pre-commit>=4.3.0", "basedpyright>=1.31.6", + "ruff>=0.14.2", ] [tool.pytest.ini_options] diff --git a/scrapers/codechef.py b/scrapers/codechef.py new file mode 100644 index 0000000..0f5636f --- /dev/null +++ b/scrapers/codechef.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import re +import sys +from typing import Any + +import httpx +from scrapling.fetchers import StealthyFetcher + +from .base import BaseScraper +from .models import ( + ContestListResult, + ContestSummary, + MetadataResult, + ProblemSummary, + TestCase, + TestsResult, +) + +BASE_URL = "https://www.codechef.com" +API_CONTESTS_ALL = "/api/list/contests/all" +API_CONTEST = "/api/contests/{contest_id}" +API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}" +PROBLEM_URL = "https://www.codechef.com/problems/{problem_id}" + +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +} +TIMEOUT_S = 15.0 +CONNECTIONS = 8 + +MEMORY_LIMIT_RE = re.compile( + r"Memory\s+[Ll]imit.*?([0-9.]+)\s*(MB|GB)", re.IGNORECASE | re.DOTALL +) + + +async def fetch_json(client: httpx.AsyncClient, path: str) -> dict: + r = await client.get(BASE_URL + path, headers=HEADERS, timeout=TIMEOUT_S) + r.raise_for_status() + return r.json() + + +def _extract_memory_limit(html: str) -> float: + m = MEMORY_LIMIT_RE.search(html) + if not m: + return 256.0 + value = float(m.group(1)) + unit = m.group(2).upper() + if unit == "GB": + return value * 1024.0 + return value + + +def _fetch_html_sync(url: str) -> str: + response = StealthyFetcher.fetch(url, headless=True, network_idle=True) + return str(response.body) + + +class CodeChefScraper(BaseScraper): + @property + def platform_name(self) -> str: + return "codechef" + + async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult: + async with httpx.AsyncClient() as client: + try: + data = await fetch_json( + client, API_CONTEST.format(contest_id=contest_id) + ) + except httpx.HTTPStatusError as e: + return self._create_metadata_error( + f"Failed to fetch contest {contest_id}: {e}", contest_id + ) + + if not data.get("problems"): + return self._create_metadata_error( + f"No problems found for contest {contest_id}", contest_id + ) + + problems = [] + for problem_code, problem_data in data["problems"].items(): + if problem_data.get("category_name") == "main": + problems.append( + ProblemSummary( + id=problem_code, + name=problem_data.get("name", problem_code), + ) + ) + + return MetadataResult( + success=True, + error="", + contest_id=contest_id, + problems=problems, + url=f"{BASE_URL}/{contest_id}", + ) + + async def scrape_contest_list(self) -> ContestListResult: + async with httpx.AsyncClient() as client: + try: + data = await fetch_json(client, API_CONTESTS_ALL) + except httpx.HTTPStatusError as e: + return self._create_contests_error(f"Failed to fetch contests: {e}") + + all_contests = data.get("future_contests", []) + data.get( + "past_contests", [] + ) + + max_num = 0 + for contest in all_contests: + contest_code = contest.get("contest_code", "") + if contest_code.startswith("START"): + match = re.match(r"START(\d+)", contest_code) + if match: + num = int(match.group(1)) + max_num = max(max_num, num) + + if max_num == 0: + return self._create_contests_error("No Starters contests found") + + contests = [] + sem = asyncio.Semaphore(CONNECTIONS) + + async def fetch_divisions(i: int) -> list[ContestSummary]: + parent_id = f"START{i}" + async with sem: + try: + parent_data = await fetch_json( + client, API_CONTEST.format(contest_id=parent_id) + ) + except Exception as e: + import sys + + print(f"Error fetching {parent_id}: {e}", file=sys.stderr) + return [] + + child_contests = parent_data.get("child_contests", {}) + if not child_contests: + return [] + + base_name = f"Starters {i}" + divisions = [] + + for div_key, div_data in child_contests.items(): + div_code = div_data.get("contest_code", "") + div_num = div_data.get("div", {}).get("div_number", "") + if div_code and div_num: + divisions.append( + ContestSummary( + id=div_code, + name=base_name, + display_name=f"{base_name} (Div. {div_num})", + ) + ) + + return divisions + + tasks = [fetch_divisions(i) for i in range(1, max_num + 1)] + for coro in asyncio.as_completed(tasks): + divisions = await coro + contests.extend(divisions) + + return ContestListResult(success=True, error="", contests=contests) + + async def stream_tests_for_category_async(self, contest_id: str) -> None: + async with httpx.AsyncClient( + limits=httpx.Limits(max_connections=CONNECTIONS) + ) as client: + try: + contest_data = await fetch_json( + client, API_CONTEST.format(contest_id=contest_id) + ) + except Exception: + return + + all_problems = contest_data.get("problems", {}) + if not all_problems: + return + + problems = { + code: data + for code, data in all_problems.items() + if data.get("category_name") == "main" + } + + sem = asyncio.Semaphore(CONNECTIONS) + + async def run_one(problem_code: str) -> dict[str, Any]: + async with sem: + try: + problem_data = await fetch_json( + client, + API_PROBLEM.format( + contest_id=contest_id, problem_id=problem_code + ), + ) + + sample_tests = ( + problem_data.get("problemComponents", {}).get( + "sampleTestCases", [] + ) + or [] + ) + tests = [ + TestCase( + input=t.get("input", "").strip(), + expected=t.get("output", "").strip(), + ) + for t in sample_tests + if not t.get("isDeleted", False) + ] + + time_limit_str = problem_data.get("max_timelimit", "1") + timeout_ms = int(float(time_limit_str) * 1000) + + problem_url = PROBLEM_URL.format(problem_id=problem_code) + loop = asyncio.get_event_loop() + html = await loop.run_in_executor( + None, _fetch_html_sync, problem_url + ) + memory_mb = _extract_memory_limit(html) + + interactive = False + + except Exception: + tests = [] + timeout_ms = 1000 + memory_mb = 256.0 + interactive = False + + return { + "problem_id": problem_code, + "tests": [ + {"input": t.input, "expected": t.expected} for t in tests + ], + "timeout_ms": timeout_ms, + "memory_mb": memory_mb, + "interactive": interactive, + } + + tasks = [run_one(problem_code) for problem_code in problems.keys()] + for coro in asyncio.as_completed(tasks): + payload = await coro + print(json.dumps(payload), flush=True) + + +async def main_async() -> int: + if len(sys.argv) < 2: + result = MetadataResult( + success=False, + error="Usage: codechef.py metadata OR codechef.py tests OR codechef.py contests", + url="", + ) + print(result.model_dump_json()) + return 1 + + mode: str = sys.argv[1] + scraper = CodeChefScraper() + + if mode == "metadata": + if len(sys.argv) != 3: + result = MetadataResult( + success=False, + error="Usage: codechef.py metadata ", + url="", + ) + print(result.model_dump_json()) + return 1 + contest_id = sys.argv[2] + result = await scraper.scrape_contest_metadata(contest_id) + print(result.model_dump_json()) + return 0 if result.success else 1 + + if mode == "tests": + if len(sys.argv) != 3: + tests_result = TestsResult( + success=False, + error="Usage: codechef.py tests ", + problem_id="", + tests=[], + timeout_ms=0, + memory_mb=0, + ) + print(tests_result.model_dump_json()) + return 1 + contest_id = sys.argv[2] + await scraper.stream_tests_for_category_async(contest_id) + return 0 + + if mode == "contests": + if len(sys.argv) != 2: + contest_result = ContestListResult( + success=False, error="Usage: codechef.py contests" + ) + print(contest_result.model_dump_json()) + return 1 + contest_result = await scraper.scrape_contest_list() + print(contest_result.model_dump_json()) + return 0 if contest_result.success else 1 + + result = MetadataResult( + success=False, + error=f"Unknown mode: {mode}. Use 'metadata ', 'tests ', or 'contests'", + url="", + ) + print(result.model_dump_json()) + return 1 + + +def main() -> None: + sys.exit(asyncio.run(main_async())) + + +if __name__ == "__main__": + main() diff --git a/spec/execute_spec.lua b/spec/execute_spec.lua deleted file mode 100644 index 12d85d2..0000000 --- a/spec/execute_spec.lua +++ /dev/null @@ -1,11 +0,0 @@ -describe('run module', function() - local run = require('cp.runner.run') - - describe('basic functionality', function() - it('can get panel state', function() - local state = run.get_panel_state() - assert.is_table(state) - assert.is_table(state.test_cases) - end) - end) -end) diff --git a/tests/conftest.py b/tests/conftest.py index dfd8e7c..fd856bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,13 +63,13 @@ def run_scraper_offline(fixture_text): target = target.removeprefix("https://cses.fi") if target.strip("/") == "problemset": - return fixture_text("cses_contests.html") + return fixture_text("cses/contests.html") if target.startswith("/problemset/task/") or target.startswith( "problemset/task/" ): pid = target.rstrip("/").split("/")[-1] - return fixture_text(f"cses_task_{pid}.html") + return fixture_text(f"cses/task_{pid}.html") raise AssertionError(f"No fixture for CSES path={path!r} url={url!r}") @@ -77,12 +77,12 @@ def run_scraper_offline(fixture_text): if not url: raise AssertionError("AtCoder expects url routing") if "/contests/archive" in url: - return fixture_text("atcoder_contests.html") + return fixture_text("atcoder/contests.html") if url.endswith("/tasks"): - return fixture_text("atcoder_abc100_tasks.html") + return fixture_text("atcoder/abc100_tasks.html") if "/tasks/" in url: slug = url.rsplit("/", 1)[-1] - return fixture_text(f"atcoder_task_{slug}.html") + return fixture_text(f"atcoder/task_{slug}.html") raise AssertionError(f"No fixture for AtCoder url={url!r}") def _router_codeforces(*, path: str | None = None, url: str | None = None) -> str: @@ -90,17 +90,17 @@ def run_scraper_offline(fixture_text): raise AssertionError("Codeforces expects url routing") if "/contest/" in url and url.endswith("/problems"): contest_id = url.rstrip("/").split("/")[-2] - return fixture_text(f"codeforces_{contest_id}_problems.html") + return fixture_text(f"codeforces/{contest_id}_problems.html") if "/contests" in url and "/problem/" not in url: - return fixture_text("codeforces_contests.html") + return fixture_text("codeforces/contests.html") if "/problem/" in url: parts = url.rstrip("/").split("/") contest_id, index = parts[-3], parts[-1] - return fixture_text(f"codeforces_{contest_id}_{index}.html") + return fixture_text(f"codeforces/{contest_id}_{index}.html") if "/problemset/problem/" in url: parts = url.rstrip("/").split("/") contest_id, index = parts[-2], parts[-1] - return fixture_text(f"codeforces_{contest_id}_{index}.html") + return fixture_text(f"codeforces/{contest_id}_{index}.html") raise AssertionError(f"No fixture for Codeforces url={url!r}") @@ -136,12 +136,12 @@ def run_scraper_offline(fixture_text): case "codeforces": - class MockPage: + class MockCodeForcesPage: def __init__(self, html: str): self.html_content = html def _mock_stealthy_fetch(url: str, **kwargs): - return MockPage(_router_codeforces(url=url)) + return MockCodeForcesPage(_router_codeforces(url=url)) def _mock_requests_get(url: str, **kwargs): if "api/contest.list" in url: @@ -176,6 +176,59 @@ def run_scraper_offline(fixture_text): "requests.get": _mock_requests_get, } + case "codechef": + + class MockResponse: + def __init__(self, json_data): + self._json_data = json_data + self.status_code = 200 + + def json(self): + return self._json_data + + def raise_for_status(self): + pass + + async def __offline_get_async(client, url: str, **kwargs): + if "/api/list/contests/all" in url: + data = json.loads(fixture_text("codechef/contests.json")) + return MockResponse(data) + if "/api/contests/START" in url and "/problems/" not in url: + contest_id = url.rstrip("/").split("/")[-1] + try: + data = json.loads( + fixture_text(f"codechef/{contest_id}.json") + ) + return MockResponse(data) + except FileNotFoundError: + raise AssertionError(f"No fixture for CodeChef url={url!r}") + if "/api/contests/START" in url and "/problems/" in url: + parts = url.rstrip("/").split("/") + contest_id = parts[-3] + problem_id = parts[-1] + data = json.loads( + fixture_text(f"codechef/{contest_id}_{problem_id}.json") + ) + return MockResponse(data) + raise AssertionError(f"No fixture for CodeChef url={url!r}") + + class MockCodeChefPage: + def __init__(self, html: str): + self.body = html + self.status = 200 + + def _mock_stealthy_fetch(url: str, **kwargs): + if "/problems/" in url: + problem_id = url.rstrip("/").split("/")[-1] + html = fixture_text(f"codechef/{problem_id}.html") + return MockCodeChefPage(html) + raise AssertionError(f"No fixture for CodeChef url={url!r}") + + return { + "__offline_get_async": __offline_get_async, + "StealthyFetcher.fetch": _mock_stealthy_fetch, + } + case _: raise AssertionError(f"Unknown scraper: {scraper_name}") @@ -192,6 +245,9 @@ def run_scraper_offline(fixture_text): ns._get_async = offline_fetches["_get_async"] elif scraper_name == "cses": httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"] # type: ignore[assignment] + elif scraper_name == "codechef": + httpx.AsyncClient.get = offline_fetches["__offline_get_async"] # type: ignore[assignment] + fetchers.StealthyFetcher.fetch = offline_fetches["StealthyFetcher.fetch"] # type: ignore[assignment] main_async = getattr(ns, "main_async") assert callable(main_async), f"main_async not found in {scraper_name}" diff --git a/tests/fixtures/atcoder_abc100_tasks.html b/tests/fixtures/atcoder/abc100_tasks.html similarity index 100% rename from tests/fixtures/atcoder_abc100_tasks.html rename to tests/fixtures/atcoder/abc100_tasks.html diff --git a/tests/fixtures/atcoder_contests.html b/tests/fixtures/atcoder/contests.html similarity index 100% rename from tests/fixtures/atcoder_contests.html rename to tests/fixtures/atcoder/contests.html diff --git a/tests/fixtures/atcoder_task_abc100_a.html b/tests/fixtures/atcoder/task_abc100_a.html similarity index 100% rename from tests/fixtures/atcoder_task_abc100_a.html rename to tests/fixtures/atcoder/task_abc100_a.html diff --git a/tests/fixtures/atcoder_task_abc100_b.html b/tests/fixtures/atcoder/task_abc100_b.html similarity index 100% rename from tests/fixtures/atcoder_task_abc100_b.html rename to tests/fixtures/atcoder/task_abc100_b.html diff --git a/tests/fixtures/atcoder_task_abc100_c.html b/tests/fixtures/atcoder/task_abc100_c.html similarity index 100% rename from tests/fixtures/atcoder_task_abc100_c.html rename to tests/fixtures/atcoder/task_abc100_c.html diff --git a/tests/fixtures/atcoder_task_abc100_d.html b/tests/fixtures/atcoder/task_abc100_d.html similarity index 100% rename from tests/fixtures/atcoder_task_abc100_d.html rename to tests/fixtures/atcoder/task_abc100_d.html diff --git a/tests/fixtures/codechef/P1209.html b/tests/fixtures/codechef/P1209.html new file mode 100644 index 0000000..2ab7eb3 --- /dev/null +++ b/tests/fixtures/codechef/P1209.html @@ -0,0 +1,4343 @@ + + + + + + + + + + + + + + + Bitcoin Market Practice Coding Problem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+
+
+
+
+ Difficulty:172 +
+
+ +
+
+ Expand +
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ Learn the building blocks of programming + languages +
+
+
+ Take our free programming courses and learn to + solve problems like these. +
+ Start Learning +
+
+

Bitcoin Market

+

+ Chef has recently started investing in + Bitcoin.
+ He assigns a + market risk level + RR + (from + 11 + to + 1010), where: +

+
    +
  • + 11 + means the market is very safe, +
  • +
  • + 1010 + means the market is very risky. +
  • +
+

+ Chef will buy Bitcoin only if + the risk level is + 44 + or less. +

+

+ Given the current risk level + RR, determine whether Chef should buy Bitcoin. +

+

+ Print "YES" if Chef should + buy, otherwise print "NO". +

+
+

Input Format

+
    +
  • + The first and only line of input contains + a single integer + RR + — the current market risk level. +
  • +
+
+
+

Output Format

+

+ Print YES if Chef should buy + Bitcoin, Otherwise, print NO. +

+

+ You may print each character of the string + in uppercase or lowercase (for example, the + strings YES, yEs, + yes, and yeS will + all be treated as identical). +

+
+

Constraints

+
+
    +
  • + 1≤R≤101 \leq R \leq 10 +
  • +
+
+

Sample 1:

+
+
+
+ Input +
+ +
+
+
+ Output +
+ +
+
+
+
+
+
2
+
+
+
YES
+
+
+
+

Explanation:

+
+

+ The current market risk is + 22.
+ Since + 22 + is not larger than + 44, the risk is small enough, and Chef will + buy Bitcoin. +

+
+

Sample 2:

+
+
+
+ Input +
+ +
+
+
+ Output +
+ +
+
+
+
+
+
4
+
+
+
YES
+
+
+
+

Explanation:

+
+

+ The current market risk is + 44.
+ Since + 44 + is not larger than + 44, the risk is small enough, and Chef will + buy Bitcoin. +

+
+

Sample 3:

+
+
+
+ Input +
+ +
+
+
+ Output +
+ +
+
+
+
+
+
5
+
+
+
NO
+
+
+
+

Explanation:

+
+

+ The current market risk is + 55.
+ Since + 55 + is larger than + 44, the risk is too much, and Chef will + not buy Bitcoin. +

+
+
+
+
+
+
+
+
+ More Info +
+
+
+ Time limit1 secs +
+
+ Memory limit1.5 GB +
+
+ Source Limit50000 Bytes +
+
+
+
+
+
+
+

+ +

+
+
+
+
+
+
+ Author(s) + +
+
+ Tester(s) +
+ kingmessi +
+
+
+ Editorialist(s) + +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
    +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
+
+
+ +
+ + +
+
+
+ +
+
+
+
+
+
+
+ #include + <bits/stdc++.h> +
+
+
+
+ using + namespace + std; +
+
+
+
+
+
+
+ int + main() + { +
+
+
+
+ // your code goes here +
+
+
+
+
+
+
+ } +
+
+
+
+
+
+
+
+
+
+
+
+
+
+   +
+
+
+
+   +
+
+
+
+ ×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×”×” +
+
+ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +
+
+ +
+
+
+
+
+
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ Visualize Code +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + +
+ + +
+
+ +
+ + diff --git a/tests/fixtures/codechef/START209.json b/tests/fixtures/codechef/START209.json new file mode 100644 index 0000000..7ef37a0 --- /dev/null +++ b/tests/fixtures/codechef/START209.json @@ -0,0 +1 @@ +{"status":"success","user":{"username":null},"code":"START209","isRatedContest":"1","isParentContestRated":"0","name":"Starters 209 (Rated till 5 star)","problems":[],"banner":"https:\/\/cdn.codechef.com\/download\/small-banner\/START209\/1760933061.png","rules":"

CodeChef: A Platform for Aspiring Programmers<\/h4>\n

CodeChef was created as a platform to help programmers make it big in the world of algorithms, computer programming, and programming contests. At CodeChef, our dedicated efforts are aimed at reviving the inner geek within you, as we proudly host a thrilling programming (coding) contest every Wednesday.<\/p>\n

About CodeChef Starters:<\/h4>\n

CodeChef Starters is a short programming contest which takes place on every Wednesday\u00a0<\/p>\n

Contest Details:<\/h4>\n