--- a +++ b/tests/unit/test_docs.py @@ -0,0 +1,267 @@ +from ehrql.docs.__main__ import generate_docs, render +from ehrql.docs.render_includes.specs import render_specs + + +def test_generate_docs(): + data = generate_docs() + + expected = {"EMIS", "TPP"} + output = {b["name"] for b in data["backends"]} + assert expected <= output + + # Find all series strings + all_series = [ + paragraph["series"] + for spec in data["specs"] + for section in spec["sections"] + for paragraph in section["paragraphs"] + ] + # Split the series string into its series and define_population components, if necessary + # assert that each component string has no leading whitespace for the first and last lines, + # and other lines have a multiple of 4 spaces + for series in all_series: + series_lines = series.split("\n") + population_lines = [] + define_population_index = next( + ( + i + for i, line in enumerate(series_lines) + if line.startswith("define_population") + ), + None, + ) + if define_population_index: + population_lines = series_lines[define_population_index:] + series_lines = series_lines[:define_population_index] + + for lines_list in [series_lines, population_lines]: + for i, line in enumerate(lines_list): + if i in [0, len(lines_list) - 1]: + assert len(line.strip()) == len(line) + else: + leading_whitespace_count = len(line) - len(line.strip()) + assert leading_whitespace_count % 4 == 0 + + +def test_render(tmp_path): + assert not set(tmp_path.iterdir()) + render(generate_docs(), tmp_path) + assert {pt.name for pt in tmp_path.iterdir()} == { + "backends.md", + "cli.md", + "language__codelists.md", + "language__dataset.md", + "language__date_arithmetic.md", + "language__frames.md", + "language__functions.md", + "language__measures.md", + "language__series.md", + "schemas", + "schemas.md", + "specs.md", + } + + +def test_render_specs(): + specs = [ + { + "id": "1", + "title": "Filtering an event frame", + "sections": [ + { + "id": "1.1", + "title": "Including rows", + "paragraphs": [ + { + "id": "1.1.1", + "title": "Take with column", + "tables": { + "e": [ + ["", "i1", "b1"], + ["1", "101", "T"], + ["2", "201", "T"], + ["2", "203", "F"], + ["3", "302", "F"], + ] + }, + "series": "e.where(e.b1).i1.sum_for_patient()", + "output": [["1", "203"], ["2", "201"], ["3", ""]], + } + ], + } + ], + } + ] + + expected = """## 1 Filtering an event frame + + +### 1.1 Including rows + + +#### 1.1.1 Take with column + +This example makes use of an event-level table named `e` containing the following data: + +| |i1|b1 | +| - | - | - | +| 1|101|T | +| 2|201|T | +| 2|203|F | +| 3|302|F | + +```python +e.where(e.b1).i1.sum_for_patient() +``` +returns the following patient series: + +| patient | value | +| - | - | +| 1|203 | +| 2|201 | +| 3| | +""" + assert render_specs(specs) == expected + + +def test_render_specs_with_multiline_series(): + specs = [ + { + "id": "1", + "title": "Logical case expressions", + "sections": [ + { + "id": "1.1", + "title": "Logical case expressions", + "paragraphs": [ + { + "id": "1.1.1", + "title": "Case with expression", + "tables": { + "p": [ + ["patient", "i1"], + ["1", "6"], + ["2", "7"], + ["3", "8"], + ["4", "9"], + ["5", ""], + ] + }, + "series": "case(\n when(p.i1 < 8).then(p.i1),\n when(p.i1 > 8).then(100),\n)", + "output": [ + ["1", "6"], + ["2", "7"], + ["3", ""], + ["4", "100"], + ["5", ""], + ], + } + ], + } + ], + } + ] + + expected = """## 1 Logical case expressions + + +### 1.1 Logical case expressions + + +#### 1.1.1 Case with expression + +This example makes use of a patient-level table named `p` containing the following data: + +| patient|i1 | +| - | - | +| 1|6 | +| 2|7 | +| 3|8 | +| 4|9 | +| 5| | + +```python +case( + when(p.i1 < 8).then(p.i1), + when(p.i1 > 8).then(100), +) +``` +returns the following patient series: + +| patient | value | +| - | - | +| 1|6 | +| 2|7 | +| 3| | +| 4|100 | +| 5| | +""" + assert render_specs(specs) == expected + + +def test_specs_with_additional_text(): + specs = [ + { + "id": "1", + "title": "Filtering an event frame", + "text": "Chapters may have additional descriptive text blocks", + "sections": [ + { + "id": "1.1", + "title": "Including rows", + "text": "Additional text can also be added at a section level", + "paragraphs": [ + { + "id": "1.1.1", + "title": "Take with column", + "text": "Further additional text can be provided for a paragraph", + "tables": { + "e": [ + ["", "i1", "b1"], + ["1", "101", "T"], + ["2", "201", "T"], + ["2", "203", "F"], + ["3", "302", "F"], + ] + }, + "series": "e.where(e.b1).i1.sum_for_patient()", + "output": [["1", "203"], ["2", "201"], ["3", ""]], + } + ], + } + ], + } + ] + assert render_specs(specs) == ( + """## 1 Filtering an event frame +Chapters may have additional descriptive text blocks + + +### 1.1 Including rows +Additional text can also be added at a section level + + +#### 1.1.1 Take with column +Further additional text can be provided for a paragraph + +This example makes use of an event-level table named `e` containing the following data: + +| |i1|b1 | +| - | - | - | +| 1|101|T | +| 2|201|T | +| 2|203|F | +| 3|302|F | + +```python +e.where(e.b1).i1.sum_for_patient() +``` +returns the following patient series: + +| patient | value | +| - | - | +| 1|203 | +| 2|201 | +| 3| | +""" + )