Skip to content

Commit 0f50df6

Browse files
authored
Merge pull request #16 from factorio-item-browser/feature/better-error-messages
2 parents cb31f14 + 3e95b80 commit 0f50df6

File tree

6 files changed

+299
-18
lines changed

6 files changed

+299
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Changelog
22

3-
## Unreleased
3+
## 2.0.1 - 2020-04-24
44

55
### Changed
66

77
- Extracted icon renderer to Go-based binary for major performance improvement.
8+
- Improved error message in the case that Factorio crashed.
89

910
### Fixed
1011

config/autoload/base.global.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
ConfigAggregator::ENABLE_CACHE => true,
1818
'debug' => false,
1919
'name' => 'Factorio Item Browser - Export',
20-
'version' => '2.0.0'
20+
'version' => '2.0.1'
2121
];
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FactorioItemBrowser\Export\Exception;
6+
7+
use Throwable;
8+
9+
/**
10+
* The exception thrown when the execution of Factorio failed.
11+
*
12+
* @author BluePsyduck <[email protected]>
13+
* @license http://opensource.org/licenses/GPL-3.0 GPL v3
14+
*/
15+
class FactorioExecutionException extends ExportException
16+
{
17+
/**
18+
* The message template of the exception.
19+
*/
20+
protected const MESSAGE = 'Factorio exited with code %d: %s';
21+
22+
/**
23+
* The regular expression to detect the line starting the error.
24+
*/
25+
protected const REGEXP_ERROR_LINE = '(^[ ]*\d+\.\d{3} Error |^Error: )';
26+
27+
/**
28+
* Initializes the exception.
29+
* @param int $exitCode
30+
* @param string $output
31+
* @param Throwable|null $previous
32+
*/
33+
public function __construct(int $exitCode, string $output, ?Throwable $previous = null)
34+
{
35+
$message = $this->extractErrorMessageFromOutput($output);
36+
parent::__construct(sprintf(self::MESSAGE, $exitCode, $message), $exitCode, $previous);
37+
}
38+
39+
/**
40+
* Extracts the actual error message from the output.
41+
* @param string $output
42+
* @return string
43+
*/
44+
protected function extractErrorMessageFromOutput(string $output): string
45+
{
46+
$errorLines = [];
47+
$errorFound = false;
48+
49+
$lines = array_reverse(explode(PHP_EOL, $output));
50+
foreach ($lines as $line) {
51+
$errorLines[] = $line;
52+
if (preg_match(self::REGEXP_ERROR_LINE, $line) > 0) {
53+
$errorFound = true;
54+
break;
55+
}
56+
}
57+
58+
if (!$errorFound) {
59+
// We were unable to detect the start of the error. So take the last 10 lines instead.
60+
$errorLines = [];
61+
foreach (array_slice($lines, 0, 10) as $line) {
62+
if (strpos($line, '>>>') !== false) {
63+
// We have our dump placeholder, so break now before we add the dump to the message.
64+
break;
65+
}
66+
$errorLines[] = $line;
67+
}
68+
}
69+
70+
return implode(PHP_EOL, array_reverse($errorLines));
71+
}
72+
}

src/Factorio/Instance.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use FactorioItemBrowser\Export\Entity\Dump\Dump;
1010
use FactorioItemBrowser\Export\Entity\InfoJson;
1111
use FactorioItemBrowser\Export\Exception\ExportException;
12+
use FactorioItemBrowser\Export\Exception\FactorioExecutionException;
1213
use FactorioItemBrowser\Export\Mod\ModFileManager;
1314
use JMS\Serializer\SerializerInterface;
1415
use Symfony\Component\Process\Process;
@@ -57,6 +58,12 @@ class Instance
5758
*/
5859
protected $instancesDirectory;
5960

61+
/**
62+
* The version of the export project.
63+
* @var string
64+
*/
65+
protected $version;
66+
6067
/**
6168
* The directory for the combination instance.
6269
* @var string
@@ -71,21 +78,24 @@ class Instance
7178
* @param SerializerInterface $exportSerializer
7279
* @param string $factorioDirectory
7380
* @param string $instancesDirectory
81+
* @param string $version
7482
*/
7583
public function __construct(
7684
Console $console,
7785
DumpExtractor $dumpExtractor,
7886
ModFileManager $modFileManager,
7987
SerializerInterface $exportSerializer,
8088
string $factorioDirectory,
81-
string $instancesDirectory
89+
string $instancesDirectory,
90+
string $version
8291
) {
8392
$this->console = $console;
8493
$this->dumpExtractor = $dumpExtractor;
8594
$this->modFileManager = $modFileManager;
8695
$this->serializer = $exportSerializer;
8796
$this->factorioDirectory = $factorioDirectory;
8897
$this->instancesDirectory = $instancesDirectory;
98+
$this->version = $version;
8999
}
90100

91101
/**
@@ -177,8 +187,9 @@ protected function createDumpInfoJson(array $modNames): InfoJson
177187

178188
$info = new InfoJson();
179189
$info->setName('Dump')
190+
->setTitle('Factorio Item Browser - Dump')
180191
->setAuthor('factorio-item-browser')
181-
->setVersion('1.0.0')
192+
->setVersion($this->version)
182193
->setFactorioVersion($baseInfo->getVersion())
183194
->setDependencies($modNames);
184195

@@ -188,11 +199,16 @@ protected function createDumpInfoJson(array $modNames): InfoJson
188199
/**
189200
* Executes the Factorio instance.
190201
* @return string
202+
* @throws ExportException
191203
*/
192204
protected function execute(): string
193205
{
194206
$process = $this->createProcess();
195207
$process->run();
208+
if (!$process->isSuccessful()) {
209+
throw new FactorioExecutionException((int) $process->getExitCode(), $process->getOutput());
210+
}
211+
196212
return $process->getOutput();
197213
}
198214

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FactorioItemBrowserTest\Export\Exception;
6+
7+
use BluePsyduck\TestHelper\ReflectionTrait;
8+
use Exception;
9+
use FactorioItemBrowser\Export\Exception\FactorioExecutionException;
10+
use PHPUnit\Framework\MockObject\MockObject;
11+
use PHPUnit\Framework\TestCase;
12+
use ReflectionException;
13+
14+
/**
15+
* The PHPUnit test of the FactorioExecutionException class.
16+
*
17+
* @author BluePsyduck <[email protected]>
18+
* @license http://opensource.org/licenses/GPL-3.0 GPL v3
19+
* @coversDefaultClass \FactorioItemBrowser\Export\Exception\FactorioExecutionException
20+
*/
21+
class FactorioExecutionExceptionTest extends TestCase
22+
{
23+
use ReflectionTrait;
24+
25+
/**
26+
* Tests the constructing.
27+
* @covers ::__construct
28+
*/
29+
public function testConstruct(): void
30+
{
31+
$output = 'abc';
32+
$errorMessage = 'def';
33+
$exitCode = 42;
34+
$expectedMessage = 'Factorio exited with code 42: def';
35+
36+
/* @var Exception&MockObject $previous */
37+
$previous = $this->createMock(Exception::class);
38+
39+
/* @var FactorioExecutionException&MockObject $exception */
40+
$exception = $this->getMockBuilder(FactorioExecutionException::class)
41+
->onlyMethods(['extractErrorMessageFromOutput'])
42+
->disableOriginalConstructor()
43+
->getMock();
44+
$exception->expects($this->once())
45+
->method('extractErrorMessageFromOutput')
46+
->with($this->identicalTo($output))
47+
->willReturn($errorMessage);
48+
49+
$exception->__construct($exitCode, $output, $previous);
50+
51+
$this->assertSame($expectedMessage, $exception->getMessage());
52+
$this->assertSame($exitCode, $exception->getCode());
53+
$this->assertSame($previous, $exception->getPrevious());
54+
}
55+
56+
/**
57+
* Provides the data for the extractErrorMessageFromOutput test.
58+
* @return array<mixed>
59+
*/
60+
public function provideExtractErrorMessageFromOutput(): array
61+
{
62+
$output1 = <<<EOT
63+
0.007 Running in headless mode
64+
0.009 Loading mod core 0.0.0 (data.lua)
65+
0.061 Loading mod base 0.18.21 (data.lua)
66+
0.235 Error Util.cpp:83: Failed to load mod "base": __base__/data.lua:89: __base__/prototypes/...
67+
stack traceback:
68+
[C]: in function 'require'
69+
__base__/data.lua:89: in main chunk
70+
EOT;
71+
$message1 = <<<EOT
72+
0.235 Error Util.cpp:83: Failed to load mod "base": __base__/data.lua:89: __base__/prototypes/...
73+
stack traceback:
74+
[C]: in function 'require'
75+
__base__/data.lua:89: in main chunk
76+
EOT;
77+
78+
$output2 = <<<EOT
79+
2.399 Checksum for script /project/data/instances/2f4a45fa-a509-a9d1-aae6-ffcf984a7a76/...
80+
2.401 Checksum for script __Dump__/control.lua: 3285258963
81+
Error: The mod Factorio Item Browser - Dump (1.0.0) caused a non-recoverable error.
82+
Please report this error to the mod author.
83+
84+
Error while running event Dump::on_init()
85+
__Dump__/map.lua:82: attempt to index global 'prototype2' (a nil value)
86+
stack traceback:
87+
__Dump__/map.lua:82: in function 'recipe'
88+
__Dump__/control.lua:37: in function <__Dump__/control.lua:4>
89+
2.448 Goodbye
90+
EOT;
91+
$message2 = <<<EOT
92+
Error: The mod Factorio Item Browser - Dump (1.0.0) caused a non-recoverable error.
93+
Please report this error to the mod author.
94+
95+
Error while running event Dump::on_init()
96+
__Dump__/map.lua:82: attempt to index global 'prototype2' (a nil value)
97+
stack traceback:
98+
__Dump__/map.lua:82: in function 'recipe'
99+
__Dump__/control.lua:37: in function <__Dump__/control.lua:4>
100+
2.448 Goodbye
101+
EOT;
102+
103+
$output3 = <<<EOT
104+
Lorem ipsum dolor sit amet.
105+
>>>TEST>>>Actual dumped data we do not want to see in the error<<<TEST<<<
106+
foo
107+
bar
108+
2.448 Goodbye
109+
EOT;
110+
$message3 = <<<EOT
111+
foo
112+
bar
113+
2.448 Goodbye
114+
EOT;
115+
116+
return [
117+
[$output1, $message1],
118+
[$output2, $message2],
119+
[$output3, $message3],
120+
];
121+
}
122+
123+
/**
124+
* Tests the extractErrorMessageFromOutput method.
125+
* @param string $output
126+
* @param string $expectedResult
127+
* @throws ReflectionException
128+
* @covers ::extractErrorMessageFromOutput
129+
* @dataProvider provideExtractErrorMessageFromOutput
130+
*/
131+
public function testExtractErrorMessageFromOutput(string $output, string $expectedResult): void
132+
{
133+
/* @var FactorioExecutionException&MockObject $exception */
134+
$exception = $this->getMockBuilder(FactorioExecutionException::class)
135+
->onlyMethods(['__construct'])
136+
->disableOriginalConstructor()
137+
->getMock();
138+
139+
$result = $this->invokeMethod($exception, 'extractErrorMessageFromOutput', $output);
140+
141+
$this->assertSame($expectedResult, $result);
142+
}
143+
}

0 commit comments

Comments
 (0)