@anonympins/fingerprint 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,297 +1,377 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\Optimization\FunctionRegistry;
8
- use Anonympins\Fingerprint\ProblemManager;
9
- use Anonympins\Fingerprint\Store\InMemoryStore;
10
- use Anonympins\Fingerprint\Store\IStore;
11
- use PHPUnit\Framework\TestCase;
12
-
13
- // Assurez-vous que cette classe est autoloadable
14
-
15
- class ProblemManagerTest extends TestCase
16
- {
17
- private ?string $configPath = null;
18
-
19
- protected function setUp(): void
20
- {
21
- // Réinitialise le singleton avant chaque test pour garantir l'isolation
22
- ProblemManager::__internal_resetInstance();
23
- FunctionRegistry::__internal_resetRegistry(); // Assure un registre de fonctions propre
24
- $this->configPath = dirname(__FILE__).'/problems.config.json';
25
- }
26
-
27
- protected function tearDown(): void
28
- {
29
- ProblemManager::__internal_resetInstance();
30
- FunctionRegistry::__internal_resetRegistry();
31
- }
32
-
33
- private function createConfigFile(array $content): void
34
- {
35
- // S'assurer que le répertoire existe
36
- if (!is_dir(dirname($this->configPath))) {
37
- mkdir(dirname($this->configPath), 0777, true);
38
- }
39
- file_put_contents($this->configPath, json_encode($content, JSON_PRETTY_PRINT));
40
- }
41
-
42
- public function testGetInstanceThrowsExceptionIfNotInitialized(): void
43
- {
44
- $this->expectException(\RuntimeException::class);
45
- $this->expectExceptionMessage('ProblemManager must be initialized with configPath and store.');
46
- ProblemManager::getInstance();
47
- }
48
-
49
- public function testGetInstanceCreatesAndReturnsInstance(): void
50
- {
51
- $storeMock = $this->createMock(IStore::class);
52
-
53
- $instance1 = ProblemManager::getInstance($this->configPath, $storeMock);
54
- $this->assertInstanceOf(ProblemManager::class, $instance1);
55
-
56
- // Les appels suivants doivent retourner la même instance
57
- $instance2 = ProblemManager::getInstance();
58
- $this->assertSame($instance1, $instance2);
59
- $this->assertTrue(ProblemManager::isInitialized());
60
- }
61
-
62
- public function testLoadProblemsHandlesMissingFile(): void
63
- {
64
- $storeMock = $this->createMock(IStore::class);
65
- $missingConfigPath = dirname(__FILE__).'/non_existent_config.json';
66
- if (file_exists($missingConfigPath)) {
67
- unlink($missingConfigPath); // S'assurer que le fichier n'existe pas
68
- }
69
-
70
- // Ne doit pas lever d'exception, mais simplement ne pas initialiser de problèmes
71
- $manager = ProblemManager::getInstance($missingConfigPath, $storeMock);
72
- $this->assertNull($manager->dispatchWork(0.5));
73
- }
74
-
75
- public function testDispatchWorkCyclesThroughProblems(): void
76
- {
77
- $storeMock = $this->createMock(IStore::class);
78
- $storeMock->method('get')->willReturn(null);
79
-
80
- // Créer un fichier de configuration avec plusieurs problèmes pour tester le cycle
81
- $this->createConfigFile([
82
- [
83
- "id" => "problem-1",
84
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
85
- ],
86
- [
87
- "id" => "problem-2",
88
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 20000]
89
- ],
90
- [
91
- "id" => "scaling-problem",
92
- "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
93
- ],
94
- [
95
- "id" => "pareto-challenge",
96
- "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
97
- ],
98
- [
99
- "id" => "problem-1",
100
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
101
- ]
102
- ]);
103
-
104
- $manager = ProblemManager::getInstance($this->configPath, $storeMock);
105
-
106
- $work1 = $manager->dispatchWork(0.5);
107
- $this->assertEquals('problem-1', $work1['problemId']);
108
-
109
- $work2 = $manager->dispatchWork(0.5);
110
- $this->assertEquals('problem-2', $work2['problemId']);
111
-
112
- // Cycle through a few more times to ensure it loops
113
- $work3 = $manager->dispatchWork(0.5);
114
- $this->assertEquals('scaling-problem', $work3['problemId']);
115
-
116
- $work4 = $manager->dispatchWork(0.5);
117
- $this->assertEquals('pareto-challenge', $work4['problemId']);
118
-
119
- $work5 = $manager->dispatchWork(0.5);
120
- $this->assertEquals('problem-1', $work5['problemId']); // Back to the start
121
- }
122
-
123
- public function testDispatchWorkScalesDifficulty(): void
124
- {
125
- $storeMock = $this->createMock(IStore::class);
126
- $storeMock->method('get')->willReturn(null);
127
-
128
- $this->createConfigFile([
129
- [
130
- "id" => "problem-1",
131
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
132
- ],
133
- [
134
- "id" => "problem-1",
135
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
136
- ],
137
- [
138
- "id" => "scaling-problem",
139
- "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
140
- ]
141
- ]);
142
-
143
- $manager = ProblemManager::getInstance($this->configPath, $storeMock);
144
-
145
- // Facteur de suspicion faible
146
- $workLow = $manager->dispatchWork(0.1);
147
- $this->assertLessThan(12000, $workLow['task']['iterations']); // 10000 * 2^0.1 ≈ 10717
148
-
149
- // Facteur de suspicion moyen - dispatch 'problem-1'
150
- $workMedium = $manager->dispatchWork(0.5);
151
- $this->assertEquals(14142, $workMedium['task']['iterations']); // 10000 * 2^0.5
152
-
153
- // Facteur de suspicion élevé - dispatch 'scaling-problem'
154
- $workHigh = $manager->dispatchWork(1.0);
155
- $this->assertEquals(200, $workHigh['task']['generations']); // 100 * 2^1.0
156
- }
157
-
158
- public function testIntegrateSolutionUpdatesStateForBetterSolution(): void
159
- {
160
- $problemId = 'problem-1';
161
-
162
- $storeMock = $this->createMock(IStore::class);
163
-
164
- $this->createConfigFile([
165
- [
166
- "id" => $problemId,
167
- "workUnit" => [
168
- "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
169
- "baseIterations" => 10000,
170
- // **LA CORRECTION** : La fonction de score est requise pour la vérification.
171
- "scoreFunction" => "test.calculateEnergy"
172
- ]
173
- ]
174
- ]);
175
-
176
- // Enregistrer une fonction de score factice pour le test.
177
- FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) { // @phpstan-ignore-line
178
- return 800.0; // Retourne la nouvelle "meilleure" énergie attendue.
179
- });
180
-
181
- $storeMock->method('get')
182
- ->with("problem-state:{$problemId}")
183
- ->willReturn(['bestEnergy' => 1000.0]);
184
-
185
- // On s'attend à ce que le store soit mis à jour avec la nouvelle meilleure solution
186
- $storeMock->expects($this->once())
187
- ->method('set')
188
- ->with(
189
- "problem-state:{$problemId}",
190
- $this->callback(function ($state) {
191
- return isset($state['bestEnergy']) && $state['bestEnergy'] === 800.0 && isset($state['bestSolution']);
192
- })
193
- );
194
-
195
- $manager = ProblemManager::getInstance($this->configPath, $storeMock);
196
-
197
- $newSolution = [
198
- 'solution' => [1, 2, 3],
199
- 'energy' => 800.0 // C'est une meilleure solution (score plus bas)
200
- ];
201
- $manager->integrateSolution($problemId, $newSolution);
202
- }
203
-
204
- public function testIntegrateSolutionDoesNotUpdateStateForWorseSolution(): void
205
- {
206
- $problemId = 'problem-1';
207
-
208
- $storeMock = $this->createMock(IStore::class);
209
-
210
- $this->createConfigFile([
211
- [
212
- "id" => $problemId,
213
- "workUnit" => [
214
- "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
215
- "baseIterations" => 10000,
216
- "scoreFunction" => "test.calculateEnergy"
217
- ]
218
- ]
219
- ]);
220
-
221
- // La fonction de score factice retourne une énergie *pire* que celle existante. // @phpstan-ignore-line
222
- FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) {
223
- return 1200.0;
224
- });
225
-
226
- $storeMock->method('get') // @phpstan-ignore-line
227
- ->with("problem-state:{$problemId}")
228
- ->willReturn(['bestEnergy' => 1000.0]);
229
-
230
- // On s'attend à ce que `set` ne soit JAMAIS appelé car la solution n'est pas meilleure
231
- $storeMock->expects($this->never())->method('set');
232
-
233
- $manager = ProblemManager::getInstance($this->configPath, $storeMock);
234
-
235
- $worseSolution = [
236
- 'solution' => [3, 2, 1], // Le contenu de la solution n'a pas d'importance pour ce test
237
- 'energy' => 1200.0 // C'est une moins bonne solution
238
- ];
239
- $manager->integrateSolution($problemId, $worseSolution);
240
- }
241
-
242
- public function testIntegrateParetoFront(): void
243
- {
244
- $problemId = 'pareto-challenge';
245
-
246
- $storeMock = $this->createMock(IStore::class);
247
-
248
- $this->createConfigFile([
249
- [
250
- "id" => "pareto-challenge",
251
- "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
252
- ]
253
- ]);
254
- // Simule le comportement réel : le store contient déjà l'état initial.
255
- $initialState = ['paretoFront' => [['solution' => 'A', 'objectives' => [10, 20]]]];
256
- $storeMock->method('get')
257
- ->with("problem-state:{$problemId}")
258
- ->willReturn($initialState);
259
-
260
- // A new solution that dominates the existing one.
261
- $newFront = [['solution' => 'B', 'objectives' => [5, 15]]];
262
-
263
- $storeMock->expects($this->once())
264
- ->method('set')
265
- ->with(
266
- "problem-state:{$problemId}",
267
- $this->callback(function ($state) {
268
- // The new front should contain only the new, dominant solution 'B'.
269
- return isset($state['paretoFront']) && count($state['paretoFront']) === 1 && $state['paretoFront'][0]['solution'] === 'B';
270
- })
271
- );
272
-
273
- $manager = ProblemManager::getInstance($this->configPath, $storeMock);
274
- $manager->integrateSolution($problemId, ['paretoFront' => $newFront]);
275
- }
276
-
277
- public function testLoadProblemsFromStore(): void
278
- {
279
- $store = new InMemoryStore();
280
- $problemId = 'problem-1';
281
-
282
- $this->createConfigFile([
283
- [
284
- "id" => "problem-1",
285
- "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000]
286
- ]
287
- ]);
288
- $initialState = ['bestEnergy' => 5000.0, 'bestSolution' => [1, 2, 3]];
289
- $store->set("problem-state:{$problemId}", $initialState);
290
-
291
- $manager = ProblemManager::getInstance($this->configPath, $store);
292
- $work = $manager->dispatchWork(0.1); // Dispatch 'problem-1'
293
-
294
- $this->assertEquals($problemId, $work['problemId']);
295
- $this->assertEquals($initialState['bestSolution'], $work['task']['initialSolution']);
296
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\Optimization\FunctionRegistry;
8
+ use Anonympins\Fingerprint\ProblemManager;
9
+ use Anonympins\Fingerprint\Store\InMemoryStore;
10
+ use Anonympins\Fingerprint\Store\IStore;
11
+ use PHPUnit\Framework\TestCase;
12
+
13
+ // Assurez-vous que cette classe est autoloadable
14
+
15
+ class ProblemManagerTest extends TestCase
16
+ {
17
+ private ?string $configPath = null;
18
+
19
+ protected function setUp(): void
20
+ {
21
+ // Réinitialise le singleton avant chaque test pour garantir l'isolation
22
+ ProblemManager::__internal_resetInstance();
23
+ FunctionRegistry::__internal_resetRegistry(); // Assure un registre de fonctions propre
24
+ $this->configPath = dirname(__FILE__).'/problems.config.json';
25
+ }
26
+
27
+ protected function tearDown(): void
28
+ {
29
+ ProblemManager::__internal_resetInstance();
30
+ FunctionRegistry::__internal_resetRegistry();
31
+ }
32
+
33
+ private function createConfigFile(array $content): void
34
+ {
35
+ // S'assurer que le répertoire existe
36
+ if (!is_dir(dirname($this->configPath))) {
37
+ mkdir(dirname($this->configPath), 0777, true);
38
+ }
39
+ file_put_contents($this->configPath, json_encode($content, JSON_PRETTY_PRINT));
40
+ }
41
+
42
+ public function testGetInstanceThrowsExceptionIfNotInitialized(): void
43
+ {
44
+ $this->expectException(\RuntimeException::class);
45
+ $this->expectExceptionMessage('ProblemManager must be initialized with configPath and store.');
46
+ ProblemManager::getInstance();
47
+ }
48
+
49
+ public function testGetInstanceCreatesAndReturnsInstance(): void
50
+ {
51
+ $storeMock = $this->createMock(IStore::class);
52
+
53
+ $instance1 = ProblemManager::getInstance($this->configPath, $storeMock);
54
+ $this->assertInstanceOf(ProblemManager::class, $instance1);
55
+
56
+ // Les appels suivants doivent retourner la même instance
57
+ $instance2 = ProblemManager::getInstance();
58
+ $this->assertSame($instance1, $instance2);
59
+ $this->assertTrue(ProblemManager::isInitialized());
60
+ }
61
+
62
+ public function testLoadProblemsHandlesMissingFile(): void
63
+ {
64
+ $storeMock = $this->createMock(IStore::class);
65
+ $missingConfigPath = dirname(__FILE__).'/non_existent_config.json';
66
+ if (file_exists($missingConfigPath)) {
67
+ unlink($missingConfigPath); // S'assurer que le fichier n'existe pas
68
+ }
69
+
70
+ // Ne doit pas lever d'exception, mais simplement ne pas initialiser de problèmes
71
+ $manager = ProblemManager::getInstance($missingConfigPath, $storeMock);
72
+ $this->assertNull($manager->dispatchWork(0.5));
73
+ }
74
+
75
+ public function testDispatchWorkCyclesThroughProblems(): void
76
+ {
77
+ $storeMock = $this->createMock(IStore::class);
78
+ $storeMock->method('get')->willReturn(null);
79
+
80
+ // Créer un fichier de configuration avec plusieurs problèmes pour tester le cycle
81
+ $this->createConfigFile([
82
+ [
83
+ "id" => "problem-1",
84
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
85
+ ],
86
+ [
87
+ "id" => "problem-2",
88
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 20000]
89
+ ],
90
+ [
91
+ "id" => "scaling-problem",
92
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
93
+ ],
94
+ [
95
+ "id" => "pareto-challenge",
96
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
97
+ ],
98
+ [
99
+ "id" => "problem-1",
100
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
101
+ ]
102
+ ]);
103
+
104
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
105
+
106
+ $work1 = $manager->dispatchWork(0.5);
107
+ $this->assertEquals('problem-1', $work1['problemId']);
108
+
109
+ $work2 = $manager->dispatchWork(0.5);
110
+ $this->assertEquals('problem-2', $work2['problemId']);
111
+
112
+ // Cycle through a few more times to ensure it loops
113
+ $work3 = $manager->dispatchWork(0.5);
114
+ $this->assertEquals('scaling-problem', $work3['problemId']);
115
+
116
+ $work4 = $manager->dispatchWork(0.5);
117
+ $this->assertEquals('pareto-challenge', $work4['problemId']);
118
+
119
+ $work5 = $manager->dispatchWork(0.5);
120
+ $this->assertEquals('problem-1', $work5['problemId']); // Back to the start
121
+ }
122
+
123
+ public function testDispatchWorkScalesDifficulty(): void
124
+ {
125
+ $storeMock = $this->createMock(IStore::class);
126
+ $storeMock->method('get')->willReturn(null);
127
+
128
+ $this->createConfigFile([
129
+ [
130
+ "id" => "problem-1",
131
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
132
+ ],
133
+ [
134
+ "id" => "problem-1",
135
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
136
+ ],
137
+ [
138
+ "id" => "scaling-problem",
139
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
140
+ ]
141
+ ]);
142
+
143
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
144
+
145
+ // Facteur de suspicion faible
146
+ $workLow = $manager->dispatchWork(0.1);
147
+ $this->assertLessThan(12000, $workLow['task']['iterations']); // 10000 * 2^0.1 ≈ 10717
148
+
149
+ // Facteur de suspicion moyen - dispatch 'problem-1'
150
+ $workMedium = $manager->dispatchWork(0.5);
151
+ $this->assertEquals(14142, $workMedium['task']['iterations']); // 10000 * 2^0.5
152
+
153
+ // Facteur de suspicion élevé - dispatch 'scaling-problem'
154
+ $workHigh = $manager->dispatchWork(1.0);
155
+ $this->assertEquals(200, $workHigh['task']['generations']); // 100 * 2^1.0
156
+ }
157
+
158
+ public function testIntegrateSolutionUpdatesStateForBetterSolution(): void
159
+ {
160
+ $problemId = 'problem-1';
161
+
162
+ $storeMock = $this->createMock(IStore::class);
163
+
164
+ $this->createConfigFile([
165
+ [
166
+ "id" => $problemId,
167
+ "workUnit" => [
168
+ "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
169
+ "baseIterations" => 10000,
170
+ // **LA CORRECTION** : La fonction de score est requise pour la vérification.
171
+ "scoreFunction" => "test.calculateEnergy"
172
+ ]
173
+ ]
174
+ ]);
175
+
176
+ // Enregistrer une fonction de score factice pour le test.
177
+ FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) { // @phpstan-ignore-line
178
+ return 800.0; // Retourne la nouvelle "meilleure" énergie attendue.
179
+ });
180
+
181
+ $storeMock->method('get')
182
+ ->with("problem-state:{$problemId}")
183
+ ->willReturn(['bestEnergy' => 1000.0]);
184
+
185
+ // On s'attend à ce que le store soit mis à jour avec la nouvelle meilleure solution
186
+ $storeMock->expects($this->once())
187
+ ->method('set')
188
+ ->with(
189
+ "problem-state:{$problemId}",
190
+ $this->callback(function ($state) {
191
+ return isset($state['bestEnergy']) && $state['bestEnergy'] === 800.0 && isset($state['bestSolution']);
192
+ })
193
+ );
194
+
195
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
196
+
197
+ $newSolution = [
198
+ 'solution' => [1, 2, 3],
199
+ 'energy' => 800.0 // C'est une meilleure solution (score plus bas)
200
+ ];
201
+ $manager->integrateSolution($problemId, $newSolution);
202
+ }
203
+
204
+ public function testIntegrateSolutionDoesNotUpdateStateForWorseSolution(): void
205
+ {
206
+ $problemId = 'problem-1';
207
+
208
+ $storeMock = $this->createMock(IStore::class);
209
+
210
+ $this->createConfigFile([
211
+ [
212
+ "id" => $problemId,
213
+ "workUnit" => [
214
+ "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
215
+ "baseIterations" => 10000,
216
+ "scoreFunction" => "test.calculateEnergy"
217
+ ]
218
+ ]
219
+ ]);
220
+
221
+ // La fonction de score factice retourne une énergie *pire* que celle existante. // @phpstan-ignore-line
222
+ FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) {
223
+ return 1200.0;
224
+ });
225
+
226
+ $storeMock->method('get') // @phpstan-ignore-line
227
+ ->with("problem-state:{$problemId}")
228
+ ->willReturn(['bestEnergy' => 1000.0]);
229
+
230
+ // On s'attend à ce que `set` ne soit JAMAIS appelé car la solution n'est pas meilleure
231
+ $storeMock->expects($this->never())->method('set');
232
+
233
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
234
+
235
+ $worseSolution = [
236
+ 'solution' => [3, 2, 1], // Le contenu de la solution n'a pas d'importance pour ce test
237
+ 'energy' => 1200.0 // C'est une moins bonne solution
238
+ ];
239
+ $manager->integrateSolution($problemId, $worseSolution);
240
+ }
241
+
242
+ public function testIntegrateSolutionUpdatesStateForBetterFacilityLocationSolution(): void
243
+ {
244
+ $problemId = 'facility-location-test';
245
+ $storeMock = $this->createMock(IStore::class);
246
+
247
+ $this->createConfigFile([
248
+ [
249
+ "id" => $problemId,
250
+ "workUnit" => [
251
+ "type" => "simulated_annealing_iterations",
252
+ "baseIterations" => 10,
253
+ "scoreFunction" => "facility.calculateEnergy"
254
+ ],
255
+ "payload" => [
256
+ "customers" => [
257
+ ["x" => 100, "y" => 100],
258
+ ["x" => 200, "y" => 200]
259
+ ],
260
+ "options" => ["fixedCostPerFacility" => 1500]
261
+ ]
262
+ ]
263
+ ]);
264
+
265
+ $storeMock->method('get')
266
+ ->with("problem-state:{$problemId}")
267
+ ->willReturn(['bestEnergy' => 99999.0]);
268
+
269
+ $storeMock->expects($this->once())
270
+ ->method('set')
271
+ ->with(
272
+ "problem-state:{$problemId}",
273
+ $this->callback(function ($state) {
274
+ return isset($state['bestEnergy']) && $state['bestEnergy'] < 99999.0 && isset($state['bestSolution']);
275
+ })
276
+ );
277
+
278
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
279
+
280
+ $newBetterSolution = [
281
+ 'solution' => [
282
+ ['x' => 100, 'y' => 100],
283
+ ['x' => 200, 'y' => 200]
284
+ ],
285
+ 'energy' => 1500.0
286
+ ];
287
+ $manager->integrateSolution($problemId, $newBetterSolution);
288
+ }
289
+
290
+ public function testIntegrateParetoFront(): void
291
+ {
292
+ $problemId = 'pareto-challenge';
293
+
294
+ $storeMock = $this->createMock(IStore::class);
295
+
296
+ $this->createConfigFile([
297
+ [
298
+ "id" => "pareto-challenge",
299
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
300
+ ]
301
+ ]);
302
+ // Simule le comportement réel : le store contient déjà l'état initial.
303
+ $initialState = ['paretoFront' => [['solution' => 'A', 'objectives' => [10, 20]]]];
304
+ $storeMock->method('get')
305
+ ->with("problem-state:{$problemId}")
306
+ ->willReturn($initialState);
307
+
308
+ // A new solution that dominates the existing one.
309
+ $newFront = [['solution' => 'B', 'objectives' => [5, 15]]];
310
+
311
+ $storeMock->expects($this->once())
312
+ ->method('set')
313
+ ->with(
314
+ "problem-state:{$problemId}",
315
+ $this->callback(function ($state) {
316
+ // The new front should contain only the new, dominant solution 'B'.
317
+ return isset($state['paretoFront']) && count($state['paretoFront']) === 1 && $state['paretoFront'][0]['solution'] === 'B';
318
+ })
319
+ );
320
+
321
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
322
+ $manager->integrateSolution($problemId, ['paretoFront' => $newFront]);
323
+ }
324
+
325
+ public function testLoadProblemsFromStore(): void
326
+ {
327
+ $store = new InMemoryStore();
328
+ $problemId = 'problem-1';
329
+
330
+ $this->createConfigFile([
331
+ [
332
+ "id" => "problem-1",
333
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000]
334
+ ]
335
+ ]);
336
+ $initialState = ['bestEnergy' => 5000.0, 'bestSolution' => [1, 2, 3]];
337
+ $store->set("problem-state:{$problemId}", $initialState);
338
+
339
+ $manager = ProblemManager::getInstance($this->configPath, $store);
340
+ $work = $manager->dispatchWork(0.1); // Dispatch 'problem-1'
341
+
342
+ $this->assertEquals($problemId, $work['problemId']);
343
+ $this->assertEquals($initialState['bestSolution'], $work['task']['initialSolution']);
344
+ }
345
+
346
+ public function testIntegrateSolutionUpdatesStateForGeneticAlgorithmGenerations(): void
347
+ {
348
+ $problemId = 'ga-problem';
349
+ $storeMock = $this->createMock(IStore::class);
350
+
351
+ $this->createConfigFile([
352
+ [
353
+ "id" => $problemId,
354
+ "workUnit" => [
355
+ "type" => "genetic_algorithm_generations",
356
+ "baseGenerations" => 10
357
+ ]
358
+ ]
359
+ ]);
360
+
361
+ $storeMock->method('get')
362
+ ->with("problem-state:{$problemId}")
363
+ ->willReturn(['population' => null]);
364
+
365
+ $storeMock->expects($this->once())
366
+ ->method('set')
367
+ ->with(
368
+ "problem-state:{$problemId}",
369
+ $this->callback(function ($state) {
370
+ return isset($state['population']) && is_array($state['population']);
371
+ })
372
+ );
373
+
374
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
375
+ $manager->integrateSolution($problemId, ['population' => [['chromosome' => [1.0], 'fitness' => -0.15]]]);
376
+ }
297
377
  }