From a9c917de2736af59e3d88d5c5a055cf6ff1f1922 Mon Sep 17 00:00:00 2001
From: Simon Rose <simon.rose@ess.eu>
Date: Mon, 26 Sep 2022 13:09:31 +0200
Subject: [PATCH] Add tests for e3-sequencer compilation

This commit also modifies how driver.makefile processes .st/.stt files.
According to the SNL documentation, .stt files should not be pre-processed.
However, driver.makefile has historically been performing the exact same
actions to both types of files, counter to what the documentation said.
---
 require-ess/tools/driver.makefile |  44 ++------
 tests/test_sequencer.py           | 160 ++++++++++++++++++++++++++++++
 tests/utils.py                    |  17 ++--
 3 files changed, 181 insertions(+), 40 deletions(-)
 create mode 100644 tests/test_sequencer.py

diff --git a/require-ess/tools/driver.makefile b/require-ess/tools/driver.makefile
index 66d3d087..c4ddc74c 100644
--- a/require-ess/tools/driver.makefile
+++ b/require-ess/tools/driver.makefile
@@ -514,7 +514,7 @@ DBDFILES += $(patsubst %.stt,%_snl.dbd,$(notdir $(filter %.stt,${SRCS})))
 DBDFILES += $(patsubst %.gt,%.dbd,$(notdir $(filter %.gt,${SRCS})))
 
 # snc location
-SNCALL=$(shell ls  -dv $(E3_SITEMODS_PATH)/sequencer/$(sequencer_VERSION)/bin/$(EPICS_HOST_ARCH) 2> /dev/null))
+SNCALL=$(shell ls  -dv $(E3_SITEMODS_PATH)/sequencer/$(sequencer_VERSION)/bin/$(EPICS_HOST_ARCH) 2> /dev/null)
 SNC=$(lastword $(SNCALL))/snc
 
 ifneq (,$(strip $(VLIBS)))
@@ -698,59 +698,35 @@ ${INSTALL_BINS}: $(addprefix ../,$(filter-out /%,${BINS})) $(filter /%,${BINS})
 
 # Create SNL code from st/stt file.
 # Important to have %.o: %.st and %.o: %.stt rule before %.o: %.c rule!
-# Preprocess in any case because docu and implemented EPICS rules mismatch here.
 
 CPPSNCFLAGS1  = $(filter -D%, ${OP_SYS_CFLAGS})
 CPPSNCFLAGS1 += $(filter-out ${OP_SYS_INCLUDE_CPPFLAGS} ,${CPPFLAGS}) ${CPPSNCFLAGS}
 CPPSNCFLAGS1 += -I $(dir $(SNC))../../include
 SNCFLAGS += -r
 
+%.i: %.st
+	@echo ">> Preprocessing $(<F)"
+	$(CPP) ${CPPSNCFLAGS1} $< > $(*F).i
 
-# 1) ESS uses 7.0.3.1 as the minimal EPICS BASE, so we don't need to check 3.13,
-# 2) We also need -c option in $(COMPILE.c) in order to compile generated source file properly
-# 3) SNC (2.1.21) should use -o, because without them, snc returns $(*F).i.c instead of $(*F).c
-#    With the EPICS standard building rule, -o and mv are used.
-#
-# Tuesday, November 28 15:59:37 CET 2017, Jeong Han Lee
-
-
-%$(OBJ) %_snl.dbd: %.st
+%.c: %.i
 	@echo ""
 	@echo ">> SNC building process .... "
 	@echo ">> SNC                  : $(SNC)"
 	@echo ">> SNC_VERSION          : $(sequencer_VERSION)"
-	@echo ">> Preprocessing $(<F)"
-	$(RM) $(*F).i
-	$(CPP) ${CPPSNCFLAGS1} $< > $(*F).i
-	@echo ">> Converting $(*F).i to $(*F).c"
-	$(RM) $@
 	@echo ">> SNC is defined as $(SNC)"
-	$(SNC) $(TARGET_SNCFLAGS) $(SNCFLAGS) $(*F).i -o $(*F).c.tmp
-	@mv $(*F).c.tmp $(*F).c
-	@echo ">> Compiling $(*F).c"
-	$(RM) $@
-	$(COMPILE.c) -c ${SNC_CFLAGS} $(*F).c
+	$(SNC) $(TARGET_SNCFLAGS) $(SNCFLAGS) $(*F).i -o $(*F).c
+
+%_snl.dbd: %.c
 	@echo ">> Building $(*F)_snl.dbd"
 	awk -F [\(\)]  '/epicsExportRegistrar/ { print "registrar (" $$2 ")"}' $(*F).c > $(*F)_snl.dbd
 
-%$(OBJ) %_snl.dbd: %.stt
+%.c: %.stt
 	@echo ""
 	@echo ">> SNC building process .... "
 	@echo ">> SNC                  : $(SNC)"
 	@echo ">> SNC_VERSION          : $(sequencer_VERSION)"
-	@echo ">> Preprocessing $(<F)"
-	$(RM) $(*F).i
-	$(CPP) ${CPPSNCFLAGS1} $< > $(*F).i
-	@echo ">> Converting $(*F).i to $(*F).c"
-	$(RM) $@
 	@echo ">> SNC is defined as $(SNC)"
-	$(SNC) $(TARGET_SNCFLAGS) $(SNCFLAGS) $(*F).i -o $(*F).c.tmp
-	@mv $(*F).c.tmp $(*F).c
-	@echo ">> Compiling $(*F).c"
-	$(RM) $@
-	$(COMPILE.c) -c ${SNC_CFLAGS} $(*F).c
-	@echo "Building $(*F)_snl.dbd"
-	awk -F [\(\)]  '/epicsExportRegistrar/ { print "registrar(" $$2 ")"}' $(*F).c > $(*F)_snl.dbd
+	$(SNC) $(TARGET_SNCFLAGS) $(SNCFLAGS) $< -o $(*F).c
 
 
 # Create GPIB code from *.gt file.
diff --git a/tests/test_sequencer.py b/tests/test_sequencer.py
new file mode 100644
index 00000000..327d0925
--- /dev/null
+++ b/tests/test_sequencer.py
@@ -0,0 +1,160 @@
+import os
+import pathlib
+import subprocess
+
+import pytest
+from git import Repo
+
+from .utils import Wrapper
+
+GITLAB_URL = "https://gitlab.esss.lu.se"
+
+TEST_SEQ_SRC = """
+program test
+
+ss ss1 {{
+    state init {{
+        when(delay(1)) {{
+            printf({});
+        }}
+        state init
+    }}
+}}
+"""
+
+
+class SequencerBuild:
+    sequencer_url = f"{GITLAB_URL}/e3/wrappers/core/e3-sequencer.git"
+
+    def __init__(self, path: pathlib.Path):
+        self.epics_base = pathlib.Path(os.getenv("EPICS_BASE"))
+        self.base_version = self.epics_base.name.split("base-")[-1]
+        assert self.base_version
+
+        self.require_version = os.getenv("E3_REQUIRE_VERSION")
+        assert self.require_version
+
+        self.version = "sequencer_test"
+
+        self.host_arch = os.getenv("EPICS_HOST_ARCH")
+        assert self.host_arch
+
+        self.path = path / "e3-sequencer"
+
+        Repo.clone_from(self.sequencer_url, self.path)
+
+        self.config_dir = self.path / "configure"
+        self.config_dir.mkdir(exist_ok=True)
+
+        (self.config_dir / "RELEASE.local").write_text(
+            f"""EPICS_BASE := {self.epics_base}
+E3_REQUIRE_VERSION := {self.require_version}"""
+        )
+        (self.config_dir / "CONFIG_MODULE.local").write_text(
+            f"E3_MODULE_VERSION := {self.version}"
+        )
+
+        self.cell_path = self.path / "cellMods"
+
+        make_arg = ["make", "-C", self.path]
+        results = subprocess.run(
+            make_arg + ["init"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            encoding="utf-8",
+        )
+        assert results.returncode == 0
+        results = subprocess.run(make_arg + ["patch"])
+        assert results.returncode == 0
+        results = subprocess.run(make_arg + ["build"])
+        assert results.returncode == 0
+        results = subprocess.run(
+            make_arg + ["cellinstall", f"E3_CELL_PATH={self.cell_path}"]
+        )
+        assert results.returncode == 0
+
+        self.snc_path = (
+            self.cell_path
+            / f"base-{self.base_version}"
+            / f"require-{self.require_version}"
+            / "sequencer"
+            / self.version
+            / "bin"
+            / self.host_arch
+            / "snc"
+        )
+        assert self.snc_path.is_file()
+
+
+@pytest.fixture(scope="module")
+def sequencer(tmp_path_factory):
+    path = tmp_path_factory.mktemp("sequencer_build")
+    yield SequencerBuild(path)
+
+
+class SequencerWrapper(Wrapper):
+    def __init__(self, sequencer: SequencerBuild, path: pathlib.Path):
+        super().__init__(path)
+        self.sequencer = sequencer
+
+        self.add_var_to_config_module("SEQUENCER_DEP_VERSION", self.sequencer.version)
+
+    def run_make(self, *args):
+        return super().run_make(
+            *args,
+            f"E3_CELL_PATH={self.sequencer.cell_path}",
+            f"SNC={self.sequencer.snc_path}",
+        )
+
+
+@pytest.fixture
+def sequence_wrapper(sequencer, tmp_path):
+    yield SequencerWrapper(sequencer, tmp_path)
+
+
+@pytest.mark.parametrize("extension", ["st", "stt"])
+def test_compile_snl_file(sequence_wrapper: SequencerWrapper, extension):
+    snl_filename = "test_filename"
+    seq_source = sequence_wrapper.module_dir / f"{snl_filename}.{extension}"
+    seq_source.write_text(TEST_SEQ_SRC.format('""'))
+
+    sequence_wrapper.add_var_to_module_makefile(
+        "SOURCES", f"{snl_filename}.{extension}"
+    )
+
+    rc, *_ = sequence_wrapper.run_make("cellbuild")
+    assert rc == 0
+
+    assert (sequence_wrapper.build_dir / f"{snl_filename}.o").is_file()
+    assert (sequence_wrapper.build_dir / f"{snl_filename}_snl.dbd").is_file()
+
+
+def test_preprocess_st_file(sequence_wrapper: SequencerWrapper):
+    snl_filename = "test_file.st"
+    seq_src = sequence_wrapper.module_dir / snl_filename
+    seq_src.write_text(
+        '#define MESSAGE "waiting\\n"\n' + TEST_SEQ_SRC.format("MESSAGE")
+    )
+
+    sequence_wrapper.add_var_to_module_makefile("SOURCES", snl_filename)
+
+    rc, *_ = sequence_wrapper.run_make("cellbuild")
+    assert rc == 0
+
+
+def test_do_not_preprocess_stt_file(sequence_wrapper: SequencerWrapper):
+    snl_filename = "test_file"
+    seq_src = sequence_wrapper.module_dir / f"{snl_filename}.stt"
+    seq_src.write_text(
+        '#define MESSAGE "waiting\\n"\n' + TEST_SEQ_SRC.format("MESSAGE")
+    )
+
+    sequence_wrapper.add_var_to_module_makefile("SOURCES", f"{snl_filename}.stt")
+
+    rc, _, errs = sequence_wrapper.run_make("cellbuild")
+    assert rc == 2
+    assert (
+        f"No rule to make target `{snl_filename}.c', needed by `{snl_filename}_snl.dbd'"
+        in errs
+    )
+    assert not (sequence_wrapper.build_dir / f"{snl_filename}.i").is_file()
diff --git a/tests/utils.py b/tests/utils.py
index 3a9958da..e8c39439 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -15,12 +15,15 @@ class Wrapper:
 
         assert "EPICS_BASE" in test_env
         assert "E3_REQUIRE_VERSION" in test_env
+        assert "EPICS_HOST_ARCH" in test_env
+
+        self.epics_base = Path(os.getenv("EPICS_BASE"))
+        self.base_version = self.epics_base.name.split("base-")[-1]
+        self.host_arch = os.getenv("EPICS_HOST_ARCH")
+        self.require_version = os.getenv("E3_REQUIRE_VERSION")
 
         e3_require_config = (
-            Path(os.environ.get("EPICS_BASE"))
-            / "require"
-            / os.environ.get("E3_REQUIRE_VERSION")
-            / "configure"
+            self.epics_base / "require" / self.require_version / "configure"
         )
 
         assert e3_require_config.is_dir()
@@ -38,6 +41,8 @@ class Wrapper:
         self.module_dir = self.path / module_path
         self.module_dir.mkdir(parents=True)
 
+        self.build_dir = self.module_dir / f"O.{self.base_version}_{self.host_arch}"
+
         self.config_dir = self.path / "configure"
         self.config_dir.mkdir()
 
@@ -61,7 +66,7 @@ REQUIRE_CONFIG:={e3_require_config}
 include $(REQUIRE_CONFIG)/CONFIG
 include $(REQUIRE_CONFIG)/RULES_SITEMODS
 """
-        (self.makefile).write_text(makefile_contents)
+        self.makefile.write_text(makefile_contents)
 
         self.module_makefile = self.path / f"{name}.Makefile"
         module_makefile_contents = """
@@ -70,7 +75,7 @@ include $(E3_REQUIRE_TOOLS)/driver.makefile
 
 EXCLUDE_ARCHS+=debug
 """
-        (self.module_makefile).write_text(module_makefile_contents)
+        self.module_makefile.write_text(module_makefile_contents)
 
         if include_dbd:
             self.add_file("test.dbd")
-- 
GitLab