From dcc0e7d23d7f00dddd48d41f24593557af531480 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 14:56:58 -0300 Subject: [PATCH 1/7] chore: add pytest dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2c54d29..253241d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ build-backend = "setuptools.build_meta" name = "nasal-api-docs" version = "0.2.0" dependencies = [ - "Jinja2>=3.1.6,<4.0.0" + "Jinja2>=3.1.6,<4", + "pytest>=8.4.2,<9" ] requires-python = ">=3.7" authors = [ From 69fd8964ca6005934d3be6549448d1ce00ccf0e3 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:16:47 -0300 Subject: [PATCH 2/7] feature: testing with pytest, GH actions - Core test: Test if API is importable. - Parser test: Tests the parser. - Generator test: Test the generated HTML and JSON. - Running pylint and flake8 in the GH Actions workflow test before pytest. - Pylint warning disabled for cyclic-import in generator.py. --- .github/workflows/tests.yml | 28 ++++++ nasal_api_docs/generator.py | 2 +- setup.cfg | 1 + tests/__init__.py | 0 tests/conftest.py | 86 ++++++++++++++++++ tests/core_test.py | 24 +++++ tests/generator_test.py | 98 ++++++++++++++++++++ tests/parser_test.py | 177 ++++++++++++++++++++++++++++++++++++ 8 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/core_test.py create mode 100644 tests/generator_test.py create mode 100644 tests/parser_test.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ebc76fe --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,28 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install flake8 pylint + + - run: flake8 nasal_api_docs --config setup.cfg + - run: pylint --rcfile setup.cfg nasal_api_docs + - run: pytest diff --git a/nasal_api_docs/generator.py b/nasal_api_docs/generator.py index a727763..7fbc835 100644 --- a/nasal_api_docs/generator.py +++ b/nasal_api_docs/generator.py @@ -27,7 +27,7 @@ from platform import python_version as pl_python_version from jinja2 import Environment, FileSystemLoader, select_autoescape -from . import __version__ +from . import __version__ # pylint: disable=cyclic-import from .filesystem import NasalFileSystem from .parser import NasalParser diff --git a/setup.cfg b/setup.cfg index e3b2363..d3ea0f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,4 @@ max-line-length = 88 [pylint] max-line-length = 88 +disable = too-few-public-methods, too-many-instance-attributes diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..89ca8f2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,86 @@ +# Copyright (C) 2012 Adrian Musceac +# Copyright (C) 2019-2026 RenanMsV +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Shared pytest configuration and fixtures for nasal_api_docs tests.""" + +from pathlib import Path +import pytest + +from nasal_api_docs import NasalAPI + + +@pytest.fixture(scope="session") +def fg_root_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: + """ + Provides a temporary fake FlightGear root directory with the minimal structure + required by tests. + + The layout will be: + tmp/ + ├── fgdata/ + │ └── Nasal/ + └── output/ + + Returns: + Path: The path to the created fgdata directory (used as fg_root_dir). + """ + tmp_root = tmp_path_factory.mktemp("tmp") + + # Create folder structure + fg_dir = tmp_root / "fgdata" + nasal_dir = fg_dir / "Nasal" + + fg_dir.mkdir(parents=True, exist_ok=True) + nasal_dir.mkdir(parents=True, exist_ok=True) + + # Create a fake FlightGear version file + (fg_dir / "version").write_text("9797.1.0", encoding="utf-8") + + # Create a small fake .nas file + (nasal_dir / "aircraft.nas").write_text( + "# This is a comment first line\n" + "# \n" + "# This is a comment third line\n" + "#\n" + "var makeNode = func(n, anotherArgument) {\n" + "\tif (isa(n, props.Node))\n" + "\t\treturn n;\n" + "\telse\n" + "\t\treturn props.globals.getNode(n, 1);\n" + "}\n", + encoding="utf-8", + ) + + return fg_dir + + +@pytest.fixture(scope="session") +def output_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: + """ + Provides a temporary output directory for generated documentation. + + Returns: + Path: The path to the created temporary output directory. + """ + # Let pytest handle temp directory creation and cleanup + out_dir = tmp_path_factory.mktemp("output") + return out_dir + + +@pytest.fixture(scope="session") +def nasal_api(fg_root_dir: Path, output_dir: Path) -> NasalAPI: # pylint: disable=W0621 + """Provides a ready-to-use NasalAPI instance for all tests.""" + return NasalAPI(fg_root_dir=fg_root_dir, output_dir=output_dir) diff --git a/tests/core_test.py b/tests/core_test.py new file mode 100644 index 0000000..af20740 --- /dev/null +++ b/tests/core_test.py @@ -0,0 +1,24 @@ +# Copyright (C) 2012 Adrian Musceac +# Copyright (C) 2019-2026 RenanMsV +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Core integration tests for the nasal_api_docs package.""" + +from nasal_api_docs import NasalAPI + + +def test_importable(): + """Ensure the main class NasalAPI can be imported.""" + assert NasalAPI is not None diff --git a/tests/generator_test.py b/tests/generator_test.py new file mode 100644 index 0000000..dbdb21b --- /dev/null +++ b/tests/generator_test.py @@ -0,0 +1,98 @@ +# Copyright (C) 2012 Adrian Musceac +# Copyright (C) 2019-2026 RenanMsV +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""HTML generator tests for the nasal_api_docs package.""" + +import json +from nasal_api_docs import NasalAPI, parser + + +def test_basic_generation(nasal_api: NasalAPI): + """Test that the API can read fg_version and generate documentation.""" + version = nasal_api.get_fg_version() + assert version.startswith("9797"), "Incorrect or missing FG version" + + html_path = nasal_api.generate_html() + json_path = nasal_api.generate_json_tree() + + assert html_path.exists(), "HTML output file not created" + assert json_path.exists(), "JSON output file not created" + + +def test_html_generation(nasal_api: NasalAPI): + """Test that the API generated a reasonable enough html.""" + html_path = nasal_api.generate_html() + + assert html_path.exists(), "HTML output file not created" + + with open(html_path, "r", encoding="utf-8") as file: + data = file.read() + + assert "Nasal API - 9797.1.0" in data, "Incorrect title." + + assert "FlightGear version: 9797.1.0 .
" in data, "Incorrect FG version." + + assert ( + "Plausible.org" + ) in data, "Missing link buttons." + + assert ( + "📄 aircraft" + ) in data, "Incorrect module link in right namespace menu." + + assert ( + "📄 aircraft" + ) in data, "Incorrect namespace title." + + assert ( + "    " + "Nasal/aircraft.nas" + ) in data, "Incorrect path of Nasal file." + + assert ( + "aircraft.makeNode ( n, " + "anotherArgument )" + ) in data, "Incorrect function name and parameters." + + assert ( + "
This is a comment first line

" + ) in data, "Incorrect comment." + + assert ( + "
This is a comment third line

" + ) in data, "Incorrect comment." + + +def test_json_generation(nasal_api: NasalAPI): + """Test that the API generated a reasonable enough json.""" + json_path = nasal_api.generate_json_tree() + + assert json_path.exists(), "JSON output file not created" + + with open(json_path, "r", encoding="utf-8") as file: + data = json.load(file) + + assert data["meta"], "Missing metadata" + + assert data["meta"]["fg_version"].startswith("9797"), ( + "Incorrect or missing FG version." + ) + + assert data["meta"]["parser_version"] == parser.NasalParser.VERSION_STR, ( + "Incorrect parser version." + ) diff --git a/tests/parser_test.py b/tests/parser_test.py new file mode 100644 index 0000000..f5ffa2e --- /dev/null +++ b/tests/parser_test.py @@ -0,0 +1,177 @@ +# Copyright (C) 2012 Adrian Musceac +# Copyright (C) 2019-2026 RenanMsV +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Unit tests for the NasalParser.""" + +from pathlib import Path +from typing import List, Tuple + +from nasal_api_docs.parser import NasalParser + + +def test_parse_basic_var_function(tmp_path: Path) -> None: + """ + Verify that the parser can detect a simple Nasal function definition. + + Example: var foo = func(...) + + Ensures: + - One function definition is detected. + - Function name and parameters are parsed correctly. + - Correct comments are attached to the parsed result. + """ + file: Path = tmp_path / "simple_var_function.nas" + file.write_text( + "# hello this is a comment\n" + "#\n" + "var foo = func(a, b) {\n \n\treturn a + b; \n}\n", + encoding="utf-8" + ) + + parser: NasalParser = NasalParser() + result: List[Tuple[str, str, List[str]]] = parser.parse_file(file) + + assert len(result) == 1, "Expected one function definition." + + name: str + params: str + comments: List[str] + name, params, comments = result[0] + + assert name == "foo", "Expected name to be 'foo'." + assert params == "a, b", "Missing or invalid params." + assert comments == ["hello this is a comment"], ( + "Expected comments to be a single line. " + "The empty comment line should have been ignored." + ) + + +def test_parse_basic_dot_function(tmp_path: Path) -> None: + """ + Verify that the parser can detect a simple Nasal dot function definition. + + Example: Class.func = func(...) + + Ensures: + - One function definition is detected. + - Function name and parameters are parsed correctly. + - Correct comments are attached to the parsed result. + """ + file: Path = tmp_path / "simple_dot_function.nas" + file.write_text( + "# hello this is a comment\n" + "#\n" + "Class.new = func(a, b) {\n" + " return a + b;\n" + "}\n", + encoding="utf-8" + ) + + parser: NasalParser = NasalParser() + result: List[Tuple[str, str, List[str]]] = parser.parse_file(file) + + assert len(result) == 1, "Expected one function definition." + + name: str + params: str + comments: List[str] + name, params, comments = result[0] + + assert name == "Class.new", "Expected name to be 'Class.new'." + assert params == "a, b", "Missing or invalid params." + assert comments == ["hello this is a comment"], ( + "Expected comments to be a single line. " + "The empty comment line should have been ignored." + ) + + +def test_parse_basic_var_class(tmp_path: Path) -> None: + """ + Verify that the parser can detect a simple Nasal var class definition. + + Example: var Class = {...} + + Ensures: + - One class definition is detected. + - Class name is parsed correctly. + - Correct comments are attached to the parsed result. + """ + file: Path = tmp_path / "simple_var_class.nas" + file.write_text( + "# hello this is a comment\n" + "#\n" + "var Class = {\n" + "}\n", + encoding="utf-8" + ) + + parser: NasalParser = NasalParser() + result: List[Tuple[str, str, List[str]]] = parser.parse_file(file) + + assert len(result) == 1, "Expected one function definition." + + name: str + params: str + comments: List[str] + name, params, comments = result[0] + + assert name == "Class.", "Expected name to be 'Class.'." + assert params == "", "Expected no parameters." + assert comments == ["hello this is a comment"], ( + "Expected comments to be a single line. " + "The empty comment line should have been ignored." + ) + + +def test_parse_basic_member_function(tmp_path: Path) -> None: + """ + Verify that the parser can detect a simple Nasal member function definition. + + Example: var Class = { init: func {...}} + + Ensures: + - One class and one member function definition are detected. + - Class and member function name is parsed correctly. + - Correct comments are attached to the parsed result. + """ + file: Path = tmp_path / "simple_member_function.nas" + file.write_text( + "# hello this is a comment\n" + "#\n" + "var Class = {\n" + "# hello this is a comment\n" + "#\n" + "\tinit: func (a, b) {}\n" + "}\n", + encoding="utf-8" + ) + + parser: NasalParser = NasalParser() + result: List[Tuple[str, str, List[str]]] = parser.parse_file(file) + + assert len(result) == 2, "Expected two definitions." + + member_name: str + member_params: str + member_comments: List[str] + member_name, member_params, member_comments = result[1] + + assert member_name == "Class.init", "Expected name to be 'Class.init'." + assert member_params == "a, b", "Missing or invalid params." + assert member_comments == ["hello this is a comment"], ( + "Expected comments to be a single line. " + "The empty comment line should have been ignored." + ) From 098ba2fdf50e50d2e0f951ddab58dde95ff88bd3 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:28:49 -0300 Subject: [PATCH 3/7] build: restrict push trigger to dev branch --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ebc76fe..44a32e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,9 @@ name: Tests on: - push: pull_request: + push: + branches: [dev] jobs: test: From c7293eae1c1b8c480dfd76b6642535393967e226 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:33:14 -0300 Subject: [PATCH 4/7] build: test actions/checkout@v6 - Due to deprecation of node 20, must update from checkout v4 to a newer. More: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44a32e9..e05d3f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 From 3b5df38c3a481ad730ceb394084896fcbc268db7 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:35:21 -0300 Subject: [PATCH 5/7] build: edit workflow name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e05d3f9..f8ace77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Lint and Test on: pull_request: From d3272dbad77a5a5284c474fae320e61d522ed914 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:37:56 -0300 Subject: [PATCH 6/7] chore: add VSCode tasks.json --- .vscode/tasks.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c761988 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "View output in a local http server", + "type": "shell", + "problemMatcher": [], + "command": "${command:python.interpreterPath}", + "args": [ + "-m", + "http.server", + "8000", + "-d", + "out", + "-b", + "127.0.0.1" + ] + }, + { + "label": "Test package", + "type": "shell", + "problemMatcher": [], + "command": "${command:python.interpreterPath}", + "args": [ + "-m", + "pytest", + "--basetemp=tmp" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file From 2d90ba2a0bf574599b9a6b7308dc149996dcb691 Mon Sep 17 00:00:00 2001 From: RenanMsV Date: Mon, 27 Apr 2026 20:44:21 -0300 Subject: [PATCH 7/7] ops: add VSCode launch.json to debug - Make sure to run the script to download FGDATA before using this. --- .vscode/launch.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5b989e4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Module", + "type": "debugpy", + "request": "launch", + "module": "nasal_api_docs", + "args": [ + "-f", + "FGROOT/FGDATA" + ] + } + ] +} \ No newline at end of file