From d4c76e76837e08e73e2d661c112228993077cb86 Mon Sep 17 00:00:00 2001 From: Marco Favorito Date: Sun, 9 Jul 2023 19:08:58 +0200 Subject: [PATCH] test: add tests for openstacks det domain --- tests/helpers/constants.py | 1 + tests/test_compiler/base.py | 91 +++++++++ tests/test_compiler/test_blocksworld_det.py | 63 +------ tests/test_compiler/test_openstacks_det.py | 197 ++++++++++++++++++++ 4 files changed, 298 insertions(+), 54 deletions(-) create mode 100644 tests/test_compiler/base.py create mode 100644 tests/test_compiler/test_openstacks_det.py diff --git a/tests/helpers/constants.py b/tests/helpers/constants.py index 4778674f..52cce102 100644 --- a/tests/helpers/constants.py +++ b/tests/helpers/constants.py @@ -33,6 +33,7 @@ BENCHMARKS_DIR = TEST_DIR / "benchmarks" BLOCKSWORLD_DIR = BENCHMARKS_DIR / "deterministic" / "BF" / "blocksworld_ppltl" +OPENSTACKS_DIR = BENCHMARKS_DIR / "deterministic" / "BF" / "openstacks_ppltl" DOCKER_DIR = TEST_DIR / "docker" diff --git a/tests/test_compiler/base.py b/tests/test_compiler/base.py new file mode 100644 index 00000000..a582080c --- /dev/null +++ b/tests/test_compiler/base.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2021 -- 2023 WhiteMech +# +# ------------------------------ +# +# This file is part of Plan4Past. +# +# Plan4Past is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Plan4Past 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Plan4Past. If not, see . +# + +"""Base test module for testing the compiler module.""" +from abc import abstractmethod +from pathlib import Path +from typing import Tuple + +import pytest +from pddl.parser.domain import DomainParser +from pddl.parser.problem import ProblemParser +from pylogics.parsers import parse_pltl + +from plan4past.compiler import Compiler +from tests.helpers.misc import check_compilation +from tests.helpers.planutils.base import BasePlannerWrapper +from tests.helpers.planutils.val import VALWrapper + + +class BaseCompilerTest: + """Base test class for compiler tests.""" + + PATH_TO_DOMAINS_DIR: Path + PATH_TO_INSTANCES_DIR: Path + + @abstractmethod + def make_formula(self, instance_id: int, domain: Path, problem: Path) -> str: + """Make the formula for the given instance.""" + raise NotImplementedError + + @abstractmethod + def get_expected_plan(self, instance_id: int) -> Tuple[str, ...]: + """Get the expected plan for the given instance.""" + raise NotImplementedError + + def get_instance_file(self, instance_id: int) -> Path: + """Get the instance file for the given instance.""" + return self.PATH_TO_INSTANCES_DIR / f"p{instance_id}.pddl" + + def get_domain_file(self, _instance_id: int) -> Path: + """Get the domain file for the given instance.""" + return self.PATH_TO_DOMAINS_DIR / "domain.pddl" + + def _test_instance( + self, instance_id: int, val: VALWrapper, planner: BasePlannerWrapper + ) -> None: + """Test the instance with the given id.""" + domain_parser = DomainParser() + problem_parser = ProblemParser() + + domain_path = self.get_domain_file(instance_id) + problem_path = self.get_instance_file(instance_id) + if not domain_path.exists(): + pytest.fail(f"Domain {domain_path} does not exist.") + if not problem_path.exists(): + pytest.fail(f"Instance {problem_path} does not exist.") + + domain = domain_parser(domain_path.read_text(encoding="utf-8")) + problem = problem_parser(problem_path.read_text(encoding="utf-8")) + goal = parse_pltl(self.make_formula(instance_id, domain_path, problem_path)) + + compiler = Compiler(domain, problem, goal) + compiler.compile() + compiled_domain, compiled_problem = compiler.result + + check_compilation( + compiled_domain, + compiled_problem, + val, + planner, + self.get_expected_plan(instance_id), + ) diff --git a/tests/test_compiler/test_blocksworld_det.py b/tests/test_compiler/test_blocksworld_det.py index 8d28e9dc..2e412a3c 100644 --- a/tests/test_compiler/test_blocksworld_det.py +++ b/tests/test_compiler/test_blocksworld_det.py @@ -21,69 +21,22 @@ # """This module contain tests for the compiler module, using the blocksworld domain.""" -from abc import abstractmethod from pathlib import Path from typing import Tuple import pytest -from pddl.parser.domain import DomainParser -from pddl.parser.problem import ProblemParser -from pylogics.parsers import parse_pltl -from plan4past.compiler import Compiler from tests.helpers.constants import BLOCKSWORLD_DIR -from tests.helpers.misc import check_compilation -from tests.helpers.planutils.base import BasePlannerWrapper -from tests.helpers.planutils.val import VALWrapper +from tests.test_compiler.base import BaseCompilerTest -class BaseTestBlocksworldDet: - """Base test class for deterministic Blocksworld experiments.""" +class TestBlocksworldDetSimpleSequence(BaseCompilerTest): + """Test class for deterministic Blocksworld experiments, simple sequence.""" - PATH_TO_DOMAIN = BLOCKSWORLD_DIR / "domain.pddl" + PATH_TO_DOMAINS_DIR = BLOCKSWORLD_DIR PATH_TO_INSTANCES_DIR = BLOCKSWORLD_DIR - - @abstractmethod - def make_formula(self, instance_id: int, domain: Path, problem: Path) -> str: - """Make the formula for the given instance.""" - raise NotImplementedError - - @abstractmethod - def get_expected_plan(self, instance_id: int) -> Tuple[str, ...]: - """Get the expected plan for the given instance.""" - raise NotImplementedError - - def _test_instance( - self, instance_id: int, val: VALWrapper, planner: BasePlannerWrapper - ) -> None: - """Test the instance with the given id.""" - domain_parser = DomainParser() - problem_parser = ProblemParser() - - domain_path = self.PATH_TO_DOMAIN - problem_path = self.PATH_TO_INSTANCES_DIR / f"p{instance_id}.pddl" - if not problem_path.exists(): - pytest.fail(f"Instance {instance_id} does not exist.") - - domain = domain_parser(domain_path.read_text(encoding="utf-8")) - problem = problem_parser(problem_path.read_text(encoding="utf-8")) - goal = parse_pltl(self.make_formula(instance_id, domain_path, problem_path)) - - compiler = Compiler(domain, problem, goal) - compiler.compile() - compiled_domain, compiled_problem = compiler.result - - check_compilation( - compiled_domain, - compiled_problem, - val, - planner, - self.get_expected_plan(instance_id), - ) - - -class TestBlocksworldDetSimpleSequence(BaseTestBlocksworldDet): - """Test class for deterministic Blocksworld experiments, simple sequence.""" + MIN_INSTANCE_ID = 2 + MAX_INSTANCE_ID = 10 def make_formula(self, instance_id: int, domain: Path, problem: Path) -> str: """ @@ -109,7 +62,9 @@ def get_expected_plan(self, instance_id: int) -> Tuple[str, ...]: result.append(f"(stack b{i} b{i + 1})") return tuple(result) - @pytest.mark.parametrize("instance_id", list(range(2, 11))) + @pytest.mark.parametrize( + "instance_id", list(range(MIN_INSTANCE_ID, MAX_INSTANCE_ID + 1)) + ) def test_run(self, instance_id, val, default_planner): """Test the instance with the given id.""" self._test_instance(instance_id, val=val, planner=default_planner) diff --git a/tests/test_compiler/test_openstacks_det.py b/tests/test_compiler/test_openstacks_det.py new file mode 100644 index 00000000..3402a719 --- /dev/null +++ b/tests/test_compiler/test_openstacks_det.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2021 -- 2023 WhiteMech +# +# ------------------------------ +# +# This file is part of Plan4Past. +# +# Plan4Past is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Plan4Past 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Plan4Past. If not, see . +# + +"""This module contain tests for the compiler module, using the openstacks domain.""" +import json +from pathlib import Path +from typing import Tuple + +import pytest + +from tests.helpers.constants import OPENSTACKS_DIR +from tests.test_compiler.base import BaseCompilerTest + + +class TestOpenStacksDet(BaseCompilerTest): + """Test class for deterministic OpenStacks experiments.""" + + PATH_TO_DOMAINS_DIR = OPENSTACKS_DIR + PATH_TO_INSTANCES_DIR = OPENSTACKS_DIR + TEGS = OPENSTACKS_DIR / "openstacks_teg.json" + MIN_INSTANCE_ID = 1 + MAX_INSTANCE_ID = 5 + EXPECTED_PLANS = { + "p01": ( + "(setup-machine-p1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o1-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o3-n1-n0 )", + "(make-product-p1-n0 )", + "(setup-machine-p2-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o5-n1-n0 )", + "(make-product-p2-n0 )", + "(ship-order-o1-n0-n1 )", + "(setup-machine-p3-n1 )", + "(start-order-o4-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o2-n1-n0 )", + "(make-product-p3-n0 )", + "(setup-machine-p4-n0 )", + "(make-product-p4-n0 )", + "(ship-order-o5-n0-n1 )", + "(setup-machine-p5-n1 )", + "(make-product-p5-n1 )", + "(ship-order-o3-n1-n2 )", + "(ship-order-o4-n2-n3 )", + "(ship-order-o2-n3-n4 )", + ), + "p02": ( + "(setup-machine-p1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o1-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o3-n1-n0 )", + "(make-product-p1-n0 )", + "(setup-machine-p2-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o4-n1-n0 )", + "(make-product-p2-n0 )", + "(ship-order-o1-n0-n1 )", + "(setup-machine-p3-n1 )", + "(start-order-o2-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o5-n1-n0 )", + "(make-product-p3-n0 )", + "(setup-machine-p4-n0 )", + "(make-product-p4-n0 )", + "(setup-machine-p5-n0 )", + "(make-product-p5-n0 )", + "(ship-order-o3-n0-n1 )", + "(ship-order-o4-n1-n2 )", + "(ship-order-o2-n2-n3 )", + "(ship-order-o5-n3-n4 )", + ), + "p03": ( + "(setup-machine-p1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o1-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o4-n1-n0 )", + "(make-product-p1-n0 )", + "(setup-machine-p2-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o5-n1-n0 )", + "(make-product-p2-n0 )", + "(ship-order-o1-n0-n1 )", + "(setup-machine-p3-n1 )", + "(start-order-o2-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o3-n1-n0 )", + "(make-product-p3-n0 )", + "(setup-machine-p4-n0 )", + "(make-product-p4-n0 )", + "(ship-order-o4-n0-n1 )", + "(ship-order-o2-n1-n2 )", + "(setup-machine-p5-n2 )", + "(make-product-p5-n2 )", + "(ship-order-o5-n2-n3 )", + "(ship-order-o3-n3-n4 )", + ), + "p04": ( + "(setup-machine-p1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o1-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o4-n1-n0 )", + "(make-product-p1-n0 )", + "(setup-machine-p2-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o5-n1-n0 )", + "(make-product-p2-n0 )", + "(ship-order-o1-n0-n1 )", + "(setup-machine-p3-n1 )", + "(start-order-o3-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o2-n1-n0 )", + "(make-product-p3-n0 )", + "(setup-machine-p4-n0 )", + "(make-product-p4-n0 )", + "(ship-order-o5-n0-n1 )", + "(setup-machine-p5-n1 )", + "(make-product-p5-n1 )", + "(ship-order-o4-n1-n2 )", + "(ship-order-o3-n2-n3 )", + "(ship-order-o2-n3-n4 )", + ), + "p05": ( + "(setup-machine-p1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o1-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o5-n1-n0 )", + "(make-product-p1-n0 )", + "(setup-machine-p2-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o4-n1-n0 )", + "(make-product-p2-n0 )", + "(ship-order-o1-n0-n1 )", + "(setup-machine-p3-n1 )", + "(start-order-o2-n1-n0 )", + "(open-new-stack-n0-n1 )", + "(start-order-o3-n1-n0 )", + "(make-product-p3-n0 )", + "(setup-machine-p4-n0 )", + "(make-product-p4-n0 )", + "(ship-order-o5-n0-n1 )", + "(ship-order-o2-n1-n2 )", + "(setup-machine-p5-n2 )", + "(make-product-p5-n2 )", + "(ship-order-o4-n2-n3 )", + "(ship-order-o3-n3-n4 )", + ), + } + + def make_formula(self, instance_id: int, domain: Path, problem: Path) -> str: + """Make the formula for the given instance.""" + tegs = json.loads(self.TEGS.read_text()) + return tegs[f"p{instance_id:02d}"] + + def get_domain_file(self, instance_id: int) -> Path: + """Get the domain file for the given instance.""" + return self.PATH_TO_DOMAINS_DIR / f"domain_p{instance_id:02d}.pddl" + + def get_instance_file(self, instance_id: int) -> Path: + """Get the instance file for the given instance.""" + return self.PATH_TO_INSTANCES_DIR / f"p{instance_id:02d}.pddl" + + def get_expected_plan(self, instance_id: int) -> Tuple[str, ...]: + """Get the expected plan for the given instance.""" + return self.EXPECTED_PLANS[f"p{instance_id:02d}"] + + @pytest.mark.parametrize( + "instance_id", list(range(MIN_INSTANCE_ID, MAX_INSTANCE_ID + 1)) + ) + def test_run(self, instance_id, val, default_planner): + """Test the instance with the given id.""" + self._test_instance(instance_id, val=val, planner=default_planner)