Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
411 changes: 411 additions & 0 deletions docs/source/drafts.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ A fully type-annotated extensively documented Python library is available for th

🔎 Overview <self>
quickstart
drafts
question-format
filters/index
spec
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/drafts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ field is quoted from source 2 while the questions are quoted from source 1 - lin
each, which is why the fields say which source they came from. The note at the end of the
solutions is what the spec makes nothing of, and `mark ignore 2/b7` is a block of the second
source named as one.
`walkthrough` is the sheet the [draft workflow page](../../../docs/source/drafts.md) walks
through, and holds every command that page shows: the questions keep the `Q1.` the sheet numbers
them with, the first question's part is typed out because the source writes the marks it is worth
beside it, the two solutions are one block that `split block` cuts in two, and one `field replace`
writes a command KaTeX defines over one it does not. The `spec.yaml` beside it is the spec that
page runs before starting the draft again, and no command here runs it.
`degrees` writes `^\circ` into the maths of both a question and the one worked solution answering
its two parts, and is the one folder whose report comes from `in2lambda.validation` over the set
the draft describes rather than from the checks over the draft itself: the solution is reported
Expand Down
93 changes: 93 additions & 0 deletions tests/fixtures/drafts/walkthrough/commands.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
[
{
"args": {
"block": "b1"
},
"by": "tests",
"command": "mark ignore"
},
{
"args": {
"text": "b3"
},
"by": "tests",
"command": "question add"
},
{
"args": {
"block": "b4"
},
"by": "tests",
"command": "mark ignore"
},
{
"args": {
"literal": "Find the volume flow rate.",
"question": "q1"
},
"by": "tests",
"command": "part add"
},
{
"args": {
"text": "b5"
},
"by": "tests",
"command": "question add"
},
{
"args": {
"question": "q2",
"text": "b6"
},
"by": "tests",
"command": "part add"
},
{
"args": {
"block": "b2"
},
"by": "tests",
"command": "mark ignore"
},
{
"args": {
"block": "b7"
},
"by": "tests",
"command": "mark ignore"
},
{
"args": {
"at": 16,
"block": "b8"
},
"by": "tests",
"command": "split block"
},
{
"args": {
"question": "q1",
"text": "b8a"
},
"by": "tests",
"command": "question solution"
},
{
"args": {
"question": "q2",
"text": "b8b"
},
"by": "tests",
"command": "question solution"
},
{
"args": {
"field": "q2.solution",
"new": "\\tfrac12",
"old": "\\half"
},
"by": "tests",
"command": "field replace"
}
]
117 changes: 117 additions & 0 deletions tests/fixtures/drafts/walkthrough/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"b1.ignore": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
1,
1
]
],
"value": true
},
"b2.ignore": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
3,
3
]
],
"value": true
},
"b4.ignore": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
7,
7
]
],
"value": true
},
"b7.ignore": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
13,
13
]
],
"value": true
},
"q1.p1.text": {
"by": "tests",
"edited": true,
"layer": 4,
"ranges": [],
"value": "Find the volume flow rate."
},
"q1.solution": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
15,
15
]
],
"value": "The flow rate is $Q = \\pi d^2 v / 4$."
},
"q1.text": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
5,
5
]
],
"value": "Q1. Water flows through a horizontal pipe of diameter $d$ at speed $v$."
},
"q2.p1.text": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
11,
11
]
],
"value": "Find the drag force on it."
},
"q2.solution": {
"by": "tests",
"edited": true,
"layer": 3,
"ranges": [
[
16,
16
]
],
"value": "The drag is $F = \\tfrac12 \\rho U^2 A C_d$."
},
"q2.text": {
"by": "tests",
"edited": false,
"layer": 3,
"ranges": [
[
9,
9
]
],
"value": "Q2. A submarine is towed at speed $U$ through still water."
}
}
16 changes: 16 additions & 0 deletions tests/fixtures/drafts/walkthrough/source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Pipe flow problems

Answer both questions, and show your working.

Q1. Water flows through a horizontal pipe of diameter $d$ at speed $v$.

(a) Find the volume flow rate. (4 marks)

Q2. A submarine is towed at speed $U$ through still water.

(a) Find the drag force on it.

## Solutions

The flow rate is $Q = \pi d^2 v / 4$.
The drag is $F = \half \rho U^2 A C_d$.
5 changes: 5 additions & 0 deletions tests/fixtures/drafts/walkthrough/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
question: Para text~'^Q\d+\.'
part: ListItem
strip: ['^Q\d+\. ']
ignore: Header
layout: PartsSepSol
105 changes: 105 additions & 0 deletions tests/test_drafts_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""The draft workflow page is run as it is printed, against the fixture it is written from.

``docs/source/drafts.md`` walks one sheet from `in2lambda source add` to `in2lambda build`,
printing every command and everything it wrote. Running the page here means a command
that says something else fails the test suite, rather than a page that goes on describing
a version of in2lambda that has gone.
"""

import json
import re
import shlex
import shutil
from pathlib import Path
from typing import Any

from click.testing import CliRunner
from conftest import DRAFTS_DIR, needs_compiler

from in2lambda.main import cli

PAGE = Path(__file__).parents[1] / "docs" / "source" / "drafts.md"
"""The page, which is read rather than generated: it is prose with commands in it."""

WALKTHROUGH = DRAFTS_DIR / "walkthrough"
"""The sheet the page walks through, which `tests/test_draft.py` covers as a draft."""

DIRECTORY = "/home/you/sheet"
"""What the page prints in place of the folder the commands were run in."""

_BLOCK = re.compile(r"^```(\w+)\n(.*?)^```", re.MULTILINE | re.DOTALL)
"""One fenced block of the page, as the language it is tagged with and its content."""


def _runs(block: str) -> list[tuple[str, str]]:
"""Each command of a console block, with everything printed under it."""
runs: list[tuple[str, list[str]]] = []
for line in block.splitlines():
if line.startswith("$ "):
runs.append((line.removeprefix("$ "), []))
else:
runs[-1][1].append(line)
return [(command, "\n".join(printed)) for command, printed in runs]


def _draft(tmp_path: Path) -> dict[str, Any]:
"""The draft as the commands run so far have left it."""
return json.loads((tmp_path / "sheet.draft.json").read_text())


def _commands(block: str, tmp_path: Path) -> None:
"""Runs a console block, checking what each command prints against the page."""
for command, printed in _runs(block):
typed = shlex.split(command)
if typed[0] == "cat":
assert (tmp_path / typed[1]).read_text().rstrip("\n") == printed
continue
result = CliRunner().invoke(cli, typed[1:])
assert result.exit_code == 0, result.output
# The folder the page names, since a command prints the path it wrote to and
# the test runs in a directory of pytest's own naming.
said = result.output.replace(str(tmp_path.resolve()), DIRECTORY)
assert said.rstrip("\n") == printed, command


def _quoted(block: str, tmp_path: Path) -> None:
"""Checks a piece of the draft the page quotes against the draft itself.

Either one field, named as the draft names it, or one entry of the log.
"""
shown = json.loads(block)
draft = _draft(tmp_path)
if "command" in shown:
assert shown in draft["log"]
else:
(key,) = shown
assert {key: draft["fields"][key]} == shown


@needs_compiler
def test_every_command_the_page_prints_says_what_the_page_says(
tmp_path: Path, monkeypatch
) -> None:
"""A reader runs the page from the top, and this runs it the same way."""
shutil.copy(WALKTHROUGH / "source.md", tmp_path / "sheet.md")
shutil.copy(WALKTHROUGH / "spec.yaml", tmp_path / "spec.yaml")
monkeypatch.chdir(tmp_path)
# Who the page records every command as having been run by, which is the default a
# command takes from the environment rather than anything the page passes.
monkeypatch.setenv("USER", "you")
monkeypatch.setenv("LOGNAME", "you")

for language, block in _BLOCK.findall(PAGE.read_text()):
if language == "bash":
_commands(block, tmp_path)
elif language == "json":
_quoted(block, tmp_path)
elif language == "markdown":
assert block == (WALKTHROUGH / "source.md").read_text()
elif language == "yaml":
assert block == (WALKTHROUGH / "spec.yaml").read_text()

# And the commands the page ran are the fixture's, so that what a reader is shown is
# what the draft tests rebuild, export and render.
log = [dict(entry, by="tests") for entry in _draft(tmp_path)["log"]]
assert log == json.loads((WALKTHROUGH / "commands.json").read_text())
Loading