Repository: projectmesa/mesa
Pull request #2390 (2024-10-19 15:14:30)

Explicitly test basic examples
This is a quick update to test_examples.py for testing the basic examples.

Presently, it only tests if the models run, no further assertions are made about e.g., results. So the main purpose of this type of test is integration testing. 

github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]      | Run time [95% CI]       |
|-----------------|--------|-------------------------|-------------------------|
| BoltzmannWealth | small  | 🔵 -3.6% [-5.0%, -2.1%] | 🔵 +0.1% [-0.1%, +0.3%] |
| BoltzmannWealth | large  | 🔵 +0.7% [+0.5%, +0.9%] | 🔵 -0.1% [-0.6%, +0.6%] |
| Schelling       | small  | 🔵 +0.1% [-0.2%, +0.5%] | 🔵 -1.0% [-1.3%, -0.8%] |
| Schelling       | large  | 🔵 +0.6% [-0.2%, +1.3%] | 🔵 -0.4% [-1.5%, +1.0%] |
| WolfSheep       | small  | 🔵 +0.9% [+0.8%, +1.1%] | 🔵 +1.1% [+1.0%, +1.3%] |
| WolfSheep       | large  | 🔵 +0.5% [-0.3%, +1.2%] | 🔵 +0.8% [-1.1%, +2.5%] |
| BoidFlockers    | small  | 🔵 +0.6% [-0.0%, +1.3%] | 🔵 -0.9% [-1.5%, -0.2%] |
| BoidFlockers    | large  | 🔵 -0.7% [-1.4%, +0.2%] | 🔵 -0.7% [-1.2%, -0.3%] |


Patch:
diff --git a/mesa/examples/basic/boltzmann_wealth_model/model.py b/mesa/examples/basic/boltzmann_wealth_model/model.py
index 249778cd..a8e96a79 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/model.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/model.py
@@ -11,8 +11,8 @@ class BoltzmannWealthModel(mesa.Model):
     highly skewed distribution of wealth.
     """
 
-    def __init__(self, n=100, width=10, height=10):
-        super().__init__()
+    def __init__(self, n=100, width=10, height=10, seed=None):
+        super().__init__(seed=seed)
         self.num_agents = n
         self.grid = mesa.space.MultiGrid(width, height, True)
 
diff --git a/mesa/examples/basic/conways_game_of_life/model.py b/mesa/examples/basic/conways_game_of_life/model.py
index 183cfd92..cc3fa996 100644
--- a/mesa/examples/basic/conways_game_of_life/model.py
+++ b/mesa/examples/basic/conways_game_of_life/model.py
@@ -7,9 +7,9 @@ from .agents import Cell
 class ConwaysGameOfLife(Model):
     """Represents the 2-dimensional array of cells in Conway's Game of Life."""
 
-    def __init__(self, width=50, height=50):
+    def __init__(self, width=50, height=50, seed=None):
         """Create a new playing area of (width, height) cells."""
-        super().__init__()
+        super().__init__(seed=seed)
         # Use a simple grid, where edges wrap around.
         self.grid = SingleGrid(width, height, torus=True)
 
diff --git a/mesa/examples/basic/virus_on_network/model.py b/mesa/examples/basic/virus_on_network/model.py
index 73abf757..04e457e8 100644
--- a/mesa/examples/basic/virus_on_network/model.py
+++ b/mesa/examples/basic/virus_on_network/model.py
@@ -36,8 +36,9 @@ class VirusOnNetwork(Model):
         virus_check_frequency=0.4,
         recovery_chance=0.3,
         gain_resistance_chance=0.5,
+        seed=None,
     ):
-        super().__init__()
+        super().__init__(seed=seed)
         self.num_nodes = num_nodes
         prob = avg_node_degree / self.num_nodes
         self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob)
@@ -56,6 +57,7 @@ class VirusOnNetwork(Model):
                 "Infected": number_infected,
                 "Susceptible": number_susceptible,
                 "Resistant": number_resistant,
+                "R over S": self.resistant_susceptible_ratio,
             }
         )
 
@@ -93,7 +95,3 @@ class VirusOnNetwork(Model):
         self.agents.shuffle_do("step")
         # collect data
         self.datacollector.collect(self)
-
-    def run_model(self, n):
-        for _ in range(n):
-            self.step()


Test patch:
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 010081aa..09f4f09a 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1,90 +1,40 @@
 # noqa: D100
-import contextlib
-import importlib
-import os.path
-import sys
-import unittest
+from mesa.examples import (
+    BoidFlockers,
+    BoltzmannWealthModel,
+    ConwaysGameOfLife,
+    Schelling,
+    VirusOnNetwork,
+)
 
 
-def test_examples_imports():
-    """Test examples imports."""
-    from mesa.examples import (
-        BoidFlockers,
-        BoltzmannWealthModel,
-        ConwaysGameOfLife,
-        Schelling,
-        VirusOnNetwork,
-    )
+def test_boltzmann_model():  # noqa: D103
+    model = BoltzmannWealthModel(seed=42)
 
-    BoltzmannWealthModel()
-    Schelling()
-    BoidFlockers()
-    ConwaysGameOfLife()
-    VirusOnNetwork()
+    for _i in range(10):
+        model.step()
 
 
-def classcase(name):  # noqa: D103
-    return "".join(x.capitalize() for x in name.replace("-", "_").split("_"))
+def test_conways_game_model():  # noqa: D103
+    model = ConwaysGameOfLife(seed=42)
+    for _i in range(10):
+        model.step()
 
 
-@unittest.skip(
-    "Skipping TextExamples, because examples folder was moved. More discussion needed."
-)
-class TestExamples(unittest.TestCase):
-    """Test examples' models.
+def test_schelling_model():  # noqa: D103
+    model = Schelling(seed=42)
+    for _i in range(10):
+        model.step()
 
-    This creates a model object and iterates it through
-    some steps.  The idea is to get code coverage, rather than to test the
-    details of each example's model.
-    """
 
-    EXAMPLES = os.path.abspath(
-        os.path.join(os.path.dirname(__file__), "../mesa/examples")
-    )
+def test_virus_on_network():  # noqa: D103
+    model = VirusOnNetwork(seed=42)
+    for _i in range(10):
+        model.step()
 
-    @contextlib.contextmanager
-    def active_example_dir(self, example):
-        """Save and restore sys.path and sys.modules."""
-        old_sys_path = sys.path[:]
-        old_sys_modules = sys.modules.copy()
-        old_cwd = os.getcwd()
-        example_path = os.path.abspath(os.path.join(self.EXAMPLES, example))
-        try:
-            sys.path.insert(0, example_path)
-            os.chdir(example_path)
-            yield
-        finally:
-            os.chdir(old_cwd)
-            added = [m for m in sys.modules if m not in old_sys_modules]
-            for mod in added:
-                del sys.modules[mod]
-            sys.modules.update(old_sys_modules)
-            sys.path[:] = old_sys_path
 
-    def test_examples(self):  # noqa: D102
-        for example in os.listdir(self.EXAMPLES):
-            if not os.path.isdir(os.path.join(self.EXAMPLES, example)):
-                continue
-            if hasattr(self, f"test_{example.replace('-', '_')}"):
-                # non-standard example; tested below
-                continue
+def test_boid_flockers():  # noqa: D103
+    model = BoidFlockers(seed=42)
 
-            print(f"testing example {example!r}")
-            with self.active_example_dir(example):
-                try:
-                    # epstein_civil_violence.py at the top level
-                    mod = importlib.import_module("model")
-                    server = importlib.import_module("server")
-                    server.server.render_model()
-                except ImportError:
-                    # <example>/epstein_civil_violence.py
-                    mod = importlib.import_module(f"{example.replace('-', '_')}.model")
-                    server = importlib.import_module(
-                        f"{example.replace('-', '_')}.server"
-                    )
-                    server.server.render_model()
-                model_class = getattr(mod, classcase(example))
-                model = model_class()
-                for _ in range(10):
-                    model.step()
-                self.assertEqual(model.steps, 10)
+    for _i in range(10):
+        model.step()