diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..f8ace77
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,29 @@
+name: Lint and Test
+
+on:
+ pull_request:
+ push:
+ branches: [dev]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - 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/.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
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
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/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 = [
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 (
+ "
"
+ ) in data, "Incorrect comment."
+
+ assert (
+ "
"
+ ) 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."
+ )