Repository: Avaiga/taipy

Issue:
Refactor the `_check_required_properties` in DataNodeConfigChecker
Currently, the `_check_required_properties` in `DataNodeConfigChecker` has many hard-coded value, making it hard for future expansion and maintenance. We want to refactor this function to make it more generic similar to the current `_check_class_type` function in `develop`

What to do:
- Refactor the `_check_required_properties` in `DataNodeConfigChecker`
- Ensure existing tests do not break due to this (the behavior should remain the same)

Discussion:

Solution patch:
diff --git a/.github/workflows/overall-tests.yml b/.github/workflows/overall-tests.yml
index ccf20c23c..a396c3ef9 100644
--- a/.github/workflows/overall-tests.yml
+++ b/.github/workflows/overall-tests.yml
@@ -20,9 +20,12 @@ jobs:
   coverage:
     timeout-minutes: 50
     runs-on: ubuntu-latest
+
     if: github.event_name == 'pull_request' && github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name
     steps:
       - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0  # Fetch full history to enable proper diff
 
       - uses: actions/setup-python@v5
         with:
@@ -38,13 +41,22 @@ jobs:
 
       - name: Pytest
         run: |
-          pipenv run pytest --cov=taipy --cov-report="xml:overall-coverage.xml" tests
+          python -m pip install xmltodict
+          pipenv run pytest --cov=taipy --cov-report=xml:${{ github.workspace }}/coverage.xml --cov-config=.coveragerc
 
-      - name: Coverage
-        uses: orgoro/coverage@v3.1
-        with:
-          coverageFile: overall-coverage.xml
-          token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Fetch base branch
+        run: |
+          git fetch origin ${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}
+
+
+      - name: Check total project coverage
+        run: |
+          python tools/coverage_check.py check-total --coverage-file ${{ github.workspace }}/coverage.xml --threshold 80
+
+      - name: Check pull request coverage
+        run: |
+          python tools/coverage_check.py check-changed --coverage-file ${{ github.workspace }}/coverage.xml --threshold 80 --base-branch ${{ github.event.pull_request.base.ref }}
 
   overall-tests:
     needs: [partial-tests]
diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.tsx
index 86bebfc2e..a959679c2 100644
--- a/frontend/taipy-gui/src/components/Taipy/Chart.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Chart.tsx
@@ -294,7 +294,7 @@ const getData = (
     data: Record<string, TraceValueType>,
     additionalDatas: Array<Record<string, TraceValueType>>,
     idx: number
-) => (idx === 0 ? data : (idx <= additionalDatas.length ? additionalDatas[idx - 1]: undefined));
+) => (idx === 0 ? data : idx <= additionalDatas.length ? additionalDatas[idx - 1] : undefined);
 
 const Chart = (props: ChartProp) => {
     const {
@@ -506,10 +506,11 @@ const Chart = (props: ChartProp) => {
             return lastDataPl.current || [];
         }
         let changed = false;
+        let baseDataPl = (lastDataPl.current.length && lastDataPl.current[0]) || {};
         const newDataPl = config.traces.map((trace, idx) => {
-            const currentData = idx < lastDataPl.current.length ? lastDataPl.current[idx] : {};
+            const currentData = (idx < lastDataPl.current.length && lastDataPl.current[idx]) || baseDataPl;
             const dataKey = idx < dataKeys.length ? dataKeys[idx] : dataKeys[0];
-            const lData = idx < dataList.length ? dataList[idx] : dataList[0];
+            const lData = (idx < dataList.length && dataList[idx]) || dataList[0];
             if (!lData || isDataRefresh(lData) || !Object.keys(lData).length) {
                 return currentData;
             }
@@ -517,12 +518,12 @@ const Chart = (props: ChartProp) => {
                 idx < config.columns?.length ? config.columns[idx] : undefined,
                 config.decimators
             )[1];
-            if (!dataKey.startsWith(dtKey) || !dtKey.length) {
+            if (!dataKey.startsWith(dtKey)) {
                 return currentData;
             }
             changed = true;
             const datum = lData[dataKey];
-            const columns = config.columns[idx];
+            const columns = config.columns[idx] || config.columns[0];
             const ret = {
                 ...getArrayValue(config.options, idx, {}),
                 type: config.types[idx],
@@ -598,6 +599,9 @@ const Chart = (props: ChartProp) => {
             if (selectedMarker) {
                 ret.selected = { marker: selectedMarker };
             }
+            if (idx == 0) {
+                baseDataPl = ret;
+            }
             return ret as Data;
         });
         if (changed) {
diff --git a/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
index c142923f8..ca463176f 100644
--- a/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
@@ -328,6 +328,13 @@ describe("Selector Component", () => {
             await userEvent.click(elt);
             expect(elt.parentElement?.querySelector("span.Mui-checked")).not.toBeNull();
         });
+        it("sets the correct height for the radio mode", async () => {
+            const height = "200px";
+            const { getByRole } = render(<Selector lov={lov} mode="radio" height={height} />);
+            const selector = getByRole("radiogroup");
+            const style = window.getComputedStyle(selector);
+            expect(style.maxHeight).toBe(height);
+          });
     });
 
     describe("Selector Component check mode", () => {
@@ -366,4 +373,11 @@ describe("Selector Component", () => {
             expect(elt3.parentElement?.querySelector("span.Mui-checked")).not.toBeNull();
         });
     });
+    it("sets the correct height for the check mode", async () => {
+        const height = "200px";
+        const { container } = render(<Selector lov={lov} mode="check" height={height} />);
+        const selector = container.querySelector(".MuiFormGroup-root");
+        const style = window.getComputedStyle(selector!);
+        expect(style.maxHeight).toBe(height);
+      });
 });
diff --git a/frontend/taipy-gui/src/components/Taipy/Selector.tsx b/frontend/taipy-gui/src/components/Taipy/Selector.tsx
index 421ffbc0c..5c59f3380 100644
--- a/frontend/taipy-gui/src/components/Taipy/Selector.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Selector.tsx
@@ -178,6 +178,18 @@ const Selector = (props: SelectorProps) => {
         }),
         [width]
     );
+    const heightSx = useMemo(() => {
+        if (!height) {
+            return undefined;
+        }
+        return {
+            maxHeight: height,
+            display: 'flex',
+            flexFlow: 'column nowrap',
+            overflowY: "auto",
+        };
+    }, [height]);
+
     const paperSx = useMemo(() => {
         let sx = paperBaseSx;
         if (height !== undefined) {
@@ -371,6 +383,7 @@ const Selector = (props: SelectorProps) => {
                                 value={dropdownValue}
                                 onChange={handleChange}
                                 className={getSuffixedClassNames(className, "-radio-group")}
+                                sx={heightSx}
                             >
                                 {lovList.map((item) => (
                                     <FormControlLabel
@@ -390,7 +403,7 @@ const Selector = (props: SelectorProps) => {
                                 ))}
                             </RadioGroup>
                         ) : (
-                            <FormGroup className={getSuffixedClassNames(className, "-check-group")}>
+                            <FormGroup className={getSuffixedClassNames(className, "-check-group")} sx={heightSx}>
                                 {lovList.map((item) => (
                                     <FormControlLabel
                                         key={item.id}
diff --git a/pyproject.toml b/pyproject.toml
index 943381566..837001344 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,7 @@ exclude = [
     "build",
     "dist",
     "releases",
+    "tools",
     ".venv",
     ".mypy_cache",
     ".pytest_cache",
diff --git a/taipy/core/config/checkers/_data_node_config_checker.py b/taipy/core/config/checkers/_data_node_config_checker.py
index 92a31fc37..e7352aa92 100644
--- a/taipy/core/config/checkers/_data_node_config_checker.py
+++ b/taipy/core/config/checkers/_data_node_config_checker.py
@@ -101,15 +101,11 @@ class _DataNodeConfigChecker(_ConfigChecker):
                 f" None or populated with a timedelta value.",
             )
 
-    def _check_required_properties(self, data_node_config_id: str, data_node_config: DataNodeConfig):
-        storage_type = data_node_config.storage_type
-        if not storage_type or storage_type not in DataNodeConfig._REQUIRED_PROPERTIES:
-            return
-
-        required_properties = DataNodeConfig._REQUIRED_PROPERTIES[storage_type]
+    @staticmethod
+    def __get_sql_required_properties(storage_type: str, dn_config_properties: Dict) -> List:
         if storage_type == DataNodeConfig._STORAGE_TYPE_VALUE_SQL:
-            if data_node_config.properties:
-                if engine := data_node_config.properties.get(DataNodeConfig._REQUIRED_DB_ENGINE_SQL_PROPERTY):
+            if dn_config_properties:
+                if engine := dn_config_properties.get(DataNodeConfig._REQUIRED_DB_ENGINE_SQL_PROPERTY):
                     if engine == DataNodeConfig._DB_ENGINE_SQLITE:
                         required_properties = [
                             DataNodeConfig._REQUIRED_DB_NAME_SQL_PROPERTY,
@@ -126,9 +122,10 @@ class _DataNodeConfigChecker(_ConfigChecker):
                             DataNodeConfig._REQUIRED_READ_QUERY_SQL_PROPERTY,
                             DataNodeConfig._REQUIRED_WRITE_QUERY_BUILDER_SQL_PROPERTY,
                         ]
+                    return required_properties
         if storage_type == DataNodeConfig._STORAGE_TYPE_VALUE_SQL_TABLE:
-            if data_node_config.properties:
-                if engine := data_node_config.properties.get(DataNodeConfig._REQUIRED_DB_ENGINE_SQL_PROPERTY):
+            if dn_config_properties:
+                if engine := dn_config_properties.get(DataNodeConfig._REQUIRED_DB_ENGINE_SQL_PROPERTY):
                     if engine == DataNodeConfig._DB_ENGINE_SQLITE:
                         required_properties = [
                             DataNodeConfig._REQUIRED_DB_NAME_SQL_PROPERTY,
@@ -143,7 +140,25 @@ class _DataNodeConfigChecker(_ConfigChecker):
                             DataNodeConfig._REQUIRED_DB_ENGINE_SQL_PROPERTY,
                             DataNodeConfig._REQUIRED_TABLE_NAME_SQL_TABLE_PROPERTY,
                         ]
-        for required_property in required_properties:
+                    return required_properties
+        return []
+
+    def __storage_type_specific_required_properties(self, storage_type: str, dn_config_properties: Dict) -> List:
+        if storage_type in (DataNodeConfig._STORAGE_TYPE_VALUE_SQL_TABLE, DataNodeConfig._STORAGE_TYPE_VALUE_SQL):
+            return self.__get_sql_required_properties(storage_type, dn_config_properties)
+        return []
+
+    def _check_required_properties(self, data_node_config_id: str, data_node_config: DataNodeConfig):
+        storage_type = data_node_config.storage_type
+        if not storage_type or storage_type not in DataNodeConfig._REQUIRED_PROPERTIES:
+            return
+
+        required_properties = DataNodeConfig._REQUIRED_PROPERTIES[storage_type]
+        required_properties.extend(
+            self.__storage_type_specific_required_properties(storage_type, data_node_config.properties)
+        )
+
+        for required_property in set(required_properties):
             if not data_node_config.properties or required_property not in data_node_config.properties:
                 if data_node_config_id == DataNodeConfig._DEFAULT_KEY:
                     self._warning(
diff --git a/taipy/core/data/pickle.py b/taipy/core/data/pickle.py
index b86e82d6c..cfa462297 100644
--- a/taipy/core/data/pickle.py
+++ b/taipy/core/data/pickle.py
@@ -31,6 +31,20 @@ class PickleDataNode(DataNode, _FileDataNodeMixin):
         data node.
     - *default_data*: The default data of the data node. It is used at the data node instantiation
         to write the data to the Pickle file.
+
+    ??? warning "To use with pickle-able data only"
+        PickleDataNode should be used only for "pickle-able" data.
+
+        Everything that's build up (recursively) from basic python types (dicts, lists,
+        primitives, objects, object references, even circular) can usually be pickled by default.
+
+        Things that are usually not pickle-able are, for example, sockets, file(handler)s,
+        database connections, custom data structure using in-memory references to Python IDs,
+        and so on.
+
+        Please refer to the
+        [pickle documentation](https://docs.python.org/3/library/pickle.html) for more information.
+
     """
 
     __STORAGE_TYPE = "pickle"
diff --git a/taipy/core/scenario/scenario.py b/taipy/core/scenario/scenario.py
index bdb565296..1a7c39141 100644
--- a/taipy/core/scenario/scenario.py
+++ b/taipy/core/scenario/scenario.py
@@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 import networkx as nx
 
 from taipy.common.config.common._validate_id import _validate_id
+from taipy.common.logger._taipy_logger import _TaipyLogger
 
 from .._entity._entity import _Entity
 from .._entity._labeled import _Labeled
@@ -106,6 +107,8 @@ class Scenario(_Entity, Submittable, _Labeled):
     id: ScenarioId
     """The unique identifier of this scenario."""
 
+    _logger = _TaipyLogger._get_logger()
+
     def __init__(
         self,
         config_id: str,
@@ -611,12 +614,32 @@ class Scenario(_Entity, Submittable, _Labeled):
         if dag.number_of_nodes() == 0:
             return True
         if not nx.is_directed_acyclic_graph(dag):
+            self._logger.error(f'The DAG of scenario "{self.id}" is not a directed acyclic graph')
             return False
         for left_node, right_node in dag.edges:
             if (isinstance(left_node, DataNode) and isinstance(right_node, Task)) or (
                 isinstance(left_node, Task) and isinstance(right_node, DataNode)
             ):
                 continue
+
+            left_node_desc = (
+                f'{left_node.__class__.__name__} "{left_node.get_label()}"'
+                if isinstance(left_node, _Labeled)
+                else left_node.__class__.__name__
+                if left_node
+                else "None"
+            )
+            right_node_desc = (
+                f'{right_node.__class__.__name__} "{right_node.get_label()}"'
+                if isinstance(right_node, _Labeled)
+                else right_node.__class__.__name__
+                if right_node
+                else "None"
+            )
+            self._logger.error(
+                f'Invalid edge detected in scenario "{self.id}": left node {left_node_desc} and right node '
+                f"{right_node_desc} must connect a Task and a DataNode"
+            )
             return False
         return True
 
diff --git a/taipy/core/sequence/sequence.py b/taipy/core/sequence/sequence.py
index b07d0e348..2e8d1c33e 100644
--- a/taipy/core/sequence/sequence.py
+++ b/taipy/core/sequence/sequence.py
@@ -16,6 +16,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 import networkx as nx
 
 from taipy.common.config.common._validate_id import _validate_id
+from taipy.common.logger._taipy_logger import _TaipyLogger
 
 from .._entity._entity import _Entity
 from .._entity._labeled import _Labeled
@@ -118,6 +119,8 @@ class Sequence(_Entity, Submittable, _Labeled):
     _MANAGER_NAME = "sequence"
     __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
+    _logger = _TaipyLogger._get_logger()
+
     id: SequenceId
     """The unique identifier of the sequence."""
 
@@ -343,15 +346,37 @@ class Sequence(_Entity, Submittable, _Labeled):
         if dag.number_of_nodes() == 0:
             return True
         if not nx.is_directed_acyclic_graph(dag):
+            self._logger.error(f'The DAG of sequence "{self.id}" is not a directed acyclic graph')
             return False
         if not nx.is_weakly_connected(dag):
+            self._logger.error(f'The DAG of sequence "{self.id}" is not weakly connected')
             return False
         for left_node, right_node in dag.edges:
             if (isinstance(left_node, DataNode) and isinstance(right_node, Task)) or (
                 isinstance(left_node, Task) and isinstance(right_node, DataNode)
             ):
                 continue
+
+            left_node_desc = (
+                f'{left_node.__class__.__name__} "{left_node.get_label()}"'
+                if isinstance(left_node, _Labeled)
+                else left_node.__class__.__name__
+                if left_node
+                else "None"
+            )
+            right_node_desc = (
+                f'{right_node.__class__.__name__} "{right_node.get_label()}"'
+                if isinstance(right_node, _Labeled)
+                else right_node.__class__.__name__
+                if right_node
+                else "None"
+            )
+            self._logger.error(
+                f'Invalid edge detected in sequence "{self.id}": left node {left_node_desc} and right node '
+                f"{right_node_desc} must connect a Task and a DataNode"
+            )
             return False
+
         return True
 
     def _get_tasks(self) -> Dict[str, Task]:
diff --git a/tools/coverage_check.py b/tools/coverage_check.py
new file mode 100644
index 000000000..4cd307d14
--- /dev/null
+++ b/tools/coverage_check.py
@@ -0,0 +1,101 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import argparse
+import xmltodict
+import sys
+import subprocess
+
+
+def check_total_coverage(coverage_file, threshold=80):
+    """Check the total project coverage."""
+    with open(coverage_file) as f:
+        data = xmltodict.parse(f.read())
+    total_coverage = float(data['coverage']['@line-rate']) * 100
+    print(f"Total Coverage: {total_coverage:.2f}%")
+    if total_coverage < threshold:
+        print(f"Total project coverage is below {threshold}%: {total_coverage:.2f}%")
+        sys.exit(1)
+
+
+def check_changed_files_coverage(coverage_file, changed_files, threshold=80):
+    """Check the coverage of changed files."""
+    with open(coverage_file) as f:
+        data = xmltodict.parse(f.read())
+
+    # Handle multiple packages in the coverage report
+    packages = data["coverage"]["packages"]["package"]
+    if not isinstance(packages, list):
+        packages = [packages]
+
+    # Extract coverage data for all files
+    files = {}
+    for package in packages:
+        classes = package["classes"]["class"]
+        if not isinstance(classes, list):
+            classes = [classes]
+        for cls in classes:
+            files[cls["@filename"]] = float(cls["@line-rate"]) * 100
+    qty = 0
+    sum_coverage = 0
+    for file in changed_files:
+        if file in files:
+            coverage = files[file]
+            print(f"Coverage for {file}: {coverage:.2f}%")
+            sum_coverage += coverage
+            qty += 1
+        else:
+            print(f"No coverage data found for {file}")
+
+    if sum_coverage/qty < threshold:
+        print(f"Coverage for changed files is below {threshold}%: {sum_coverage/qty:.2f}%")
+        sys.exit(1)
+    print(f"Coverage for changed files: {sum_coverage/qty:.2f}%")
+
+
+def get_changed_files(base_branch):
+    """Get the list of changed Python files in the pull request."""
+    try:
+        result = subprocess.run(
+            ['git', 'diff', '--name-only', f"origin/{base_branch}", '--', '*.py'],
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+        changed_files = [
+            file.replace("taipy/", "") for file in result.stdout.strip().splitlines() if not file.startswith(('tests/', 'tools/'))
+        ]
+        return changed_files
+    except subprocess.CalledProcessError as e:
+        print(f"Error fetching changed files: {e}")
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description="Coverage check script.")
+    parser.add_argument('command', choices=['check-total', 'check-changed'], help="Command to execute")
+    parser.add_argument('--coverage-file', default='coverage.xml', help="Path to the coverage XML file")
+    parser.add_argument('--threshold', type=float, default=80, help="Coverage threshold percentage")
+    parser.add_argument('--base-branch', help="Base branch for comparing changed files")
+
+    args = parser.parse_args()
+
+    if args.command == 'check-total':
+        check_total_coverage(args.coverage_file, args.threshold)
+    elif args.command == 'check-changed':
+        if not args.base_branch:
+            print("Error: --base-branch is required for check-changed")
+            sys.exit(1)
+        changed_files = get_changed_files(args.base_branch)
+        if not changed_files:
+            print("No relevant Python files changed.")
+            sys.exit(0)
+        check_changed_files_coverage(args.coverage_file, changed_files, args.threshold)


Test patch:
diff --git a/tests/core/scenario/test_scenario.py b/tests/core/scenario/test_scenario.py
index 72899c20e..515fe369f 100644
--- a/tests/core/scenario/test_scenario.py
+++ b/tests/core/scenario/test_scenario.py
@@ -1538,3 +1538,25 @@ def test_check_consistency():
     task_5 = Task("bob", {}, print, [data_node_5], [data_node_3], TaskId("t5"))
     scenario_9 = Scenario("scenario_9", [task_1, task_2, task_3, task_4, task_5], {}, [], scenario_id=ScenarioId("s8"))
     assert scenario_9._is_consistent()
+
+
+def test_check_inconsistency(caplog):
+    class FakeDataNode:
+        config_id = "config_id_of_a_fake_dn"
+
+    data_node_1 = InMemoryDataNode("foo", Scope.SCENARIO, "s1")
+    data_node_2 = InMemoryDataNode("bar", Scope.SCENARIO, "s2")
+
+    task_1 = Task("grault", {}, print, [data_node_1, data_node_2], [FakeDataNode()], TaskId("t1"))
+    task_2 = Task("garply", {}, print, [data_node_1], [data_node_2], id=TaskId("t2"))
+    scenario_1 = Scenario("scenario_1", [task_1, task_2], {}, [], scenario_id=ScenarioId("s1"))
+    assert not scenario_1._is_consistent()
+    assert (
+        'Invalid edge detected in scenario "s1": left node Task "grault" and right node FakeDataNode'
+        " must connect a Task and a DataNode" in caplog.text
+    )
+
+    task_3 = Task("waldo", {}, print, [data_node_2], [data_node_1], id=TaskId("t3"))
+    scenario_2 = Scenario("scenario_2", [task_2, task_3], {}, [], scenario_id=ScenarioId("s2"))
+    assert not scenario_2._is_consistent()
+    assert 'The DAG of scenario "s2" is not a directed acyclic graph' in caplog.text
diff --git a/tests/core/sequence/test_sequence.py b/tests/core/sequence/test_sequence.py
index 9b78cb53d..f5cb7ca53 100644
--- a/tests/core/sequence/test_sequence.py
+++ b/tests/core/sequence/test_sequence.py
@@ -30,6 +30,10 @@ from taipy.core.task._task_manager import _TaskManager
 from taipy.core.task.task import Task, TaskId
 
 
+class FakeDataNode:
+    config_id = "config_id_of_a_fake_dn"
+
+
 def test_sequence_equals():
     task_config = Config.configure_task("mult_by_3", print, [], None)
     scenario_config = Config.configure_scenario("scenario", [task_config])
@@ -143,7 +147,7 @@ def test_get_set_attribute():
         sequence.bar = "KeyAlreadyUsed"
 
 
-def test_check_consistency():
+def test_check_consistency(caplog):
     sequence_1 = Sequence({}, [], "name_1")
     assert sequence_1._is_consistent()
 
@@ -157,6 +161,7 @@ def test_check_consistency():
     task_3 = Task("tfoo", {}, print, [data_node_3], [data_node_3], TaskId("task_id_3"))
     sequence_3 = Sequence({}, [task_3], "name_3")
     assert not sequence_3._is_consistent()  # Not a dag
+    assert 'The DAG of sequence "name_3" is not a directed acyclic graph' in caplog.text
 
     input_4 = InMemoryDataNode("foo", Scope.SCENARIO)
     output_4 = InMemoryDataNode("bar", Scope.SCENARIO)
@@ -164,9 +169,7 @@ def test_check_consistency():
     task_4_2 = Task("tbar", {}, print, [output_4], [input_4], TaskId("task_id_4_2"))
     sequence_4 = Sequence({}, [task_4_1, task_4_2], "name_4")
     assert not sequence_4._is_consistent()  # Not a Dag
-
-    class FakeDataNode:
-        config_id = "config_id_of_a_fake_dn"
+    assert 'The DAG of sequence "name_4" is not a directed acyclic graph' in caplog.text
 
     input_6 = DataNode("foo", Scope.SCENARIO, "input_id_5")
     output_6 = DataNode("bar", Scope.SCENARIO, "output_id_5")
@@ -174,6 +177,10 @@ def test_check_consistency():
     task_6_2 = Task("tbar", {}, print, [output_6], [FakeDataNode()], TaskId("task_id_5_2"))
     sequence_6 = Sequence({}, [task_6_1, task_6_2], "name_5")
     assert not sequence_6._is_consistent()  # Not a DataNode
+    assert (
+        'Invalid edge detected in sequence "name_5": left node Task "tbar" and right node FakeDataNode '
+        "must connect a Task and a DataNode" in caplog.text
+    )
 
     intermediate_7 = DataNode("foo", Scope.SCENARIO, "intermediate_id_7")
     output_7 = DataNode("bar", Scope.SCENARIO, "output_id_7")
@@ -197,6 +204,7 @@ def test_check_consistency():
     task_9_2 = Task("tbar", {}, print, [input_9_2], [output_9_2], TaskId("task_id_9_2"))
     sequence_9 = Sequence({}, [task_9_1, task_9_2], "name_9")
     assert not sequence_9._is_consistent()  # Not connected
+    assert 'The DAG of sequence "name_9" is not weakly connected' in caplog.text
 
     input_10_1 = DataNode("foo", Scope.SCENARIO, "output_id_10_1")
     intermediate_10_1 = DataNode("bar", Scope.SCENARIO, "intermediate_id_10_1")