Test Failed
Push — master ( a412f3...37f3d4 )
by Gerrit
08:22
created

SqlParserClassTest::dumpNodes()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 5
nop 3
dl 0
loc 25
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright (C) 2019  Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 *
8
 * @license GPL-3.0
9
 * @author Gerrit Addiks <[email protected]>
10
 */
11
12
namespace Addiks\StoredSQL\Tests\Unit\Parsing;
13
14
use Addiks\StoredSQL\Lexing\SqlTokenizer;
15
use Addiks\StoredSQL\Lexing\SqlTokenizerClass;
16
use Addiks\StoredSQL\Lexing\SqlTokens;
17
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstColumn;
18
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstConjunction;
19
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstOperation;
20
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstRoot;
21
use Addiks\StoredSQL\Parsing\SqlParserClass;
22
use Closure;
23
use PHPUnit\Framework\MockObject\MockObject;
24
use PHPUnit\Framework\TestCase;
25
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstLiteral;
26
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstOrderBy;
27
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstParenthesis;
28
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstFrom;
29
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstJoin;
30
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstSelect;
31
use Addiks\StoredSQL\Parsing\SqlParser;
32
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstNode;
33
use Webmozart\Assert\Assert;
34
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstWhere;
35
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstUpdate;
36
use Addiks\StoredSQL\Exception\UnparsableSqlException;
37
38
final class SqlParserClassTest extends TestCase
39
{
40
    const DATA_FOLDER_NAME = '../../../fixtures';
41
42
    private SqlParserClass $subject;
43
44
    /** @var MockObject&SqlTokenizer */
45
    private SqlTokenizer $tokenizer;
46
47
    /** @var array<callable> $mutators */
48
    private array $mutators;
49
50
    public function setUp(): void
51
    {
52
        $this->tokenizer = $this->createMock(SqlTokenizer::class);
53
        $this->mutators = array(function () {});
54
55
        $this->subject = new SqlParserClass($this->tokenizer, $this->mutators);
56
    }
57
58
    /**
59
     * @test
60
     * @covers SqlParserClass::parseSql
61
     */
62
    public function shouldParseSql(): void
63
    {
64
        /** @var string $sql */
65
        $sql = 'Some test-SQL snippet';
66
67
        /** @var MockObject&SqlTokens $tokens */
68
        $tokens = $this->createMock(SqlTokens::class);
69
70
        $this->tokenizer->expects($this->once())->method('tokenize')->with($this->equalTo($sql))->willReturn($tokens);
0 ignored issues
show
Bug introduced by
The method expects() does not exist on Addiks\StoredSQL\Lexing\SqlTokenizer. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

70
        $this->tokenizer->/** @scrutinizer ignore-call */ 
71
                          expects($this->once())->method('tokenize')->with($this->equalTo($sql))->willReturn($tokens);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
71
72
        $tokens->expects($this->once())->method('withoutWhitespace')->willReturn($tokens);
73
        $tokens->expects($this->once())->method('withoutComments')->willReturn($tokens);
74
75
        /** @var MockObject&SqlAstRoot $syntaxTree */
76
        $syntaxTree = $this->createMock(SqlAstRoot::class);
77
78
        $tokens->expects($this->once())->method('convertToSyntaxTree')->willReturn($syntaxTree);
79
80
        $syntaxTree->expects($this->once())->method('walk')->with($this->equalTo($this->mutators));
81
82
        $this->subject->parseSql($sql);
83
    }
84
85
    /**
86
     * @test
87
     * @covers SqlParserClass::defaultMutators
88
     */
89
    public function shouldProvideDefaultMutators(): void
90
    {
91
        $this->assertEquals([
92
            Closure::fromCallable([SqlAstLiteral::class, 'mutateAstNode']),
93
            Closure::fromCallable([SqlAstColumn::class, 'mutateAstNode']),
94
            Closure::fromCallable([SqlAstOperation::class, 'mutateAstNode']),
95
            Closure::fromCallable([SqlAstConjunction::class, 'mutateAstNode']),
96
            Closure::fromCallable([SqlAstWhere::class, 'mutateAstNode']),
97
            Closure::fromCallable([SqlAstOrderBy::class, 'mutateAstNode']),
98
            Closure::fromCallable([SqlAstParenthesis::class, 'mutateAstNode']),
99
            Closure::fromCallable([SqlAstFrom::class, 'mutateAstNode']),
100
            Closure::fromCallable([SqlAstJoin::class, 'mutateAstNode']),
101
            Closure::fromCallable([SqlAstSelect::class, 'mutateAstNode']),
102
            Closure::fromCallable([SqlAstUpdate::class, 'mutateAstNode']),
103
        ], SqlParserClass::defaultMutators());
104
    }
105
106
    /**
107
     * @test
108
     * @covers SqlParserClass::defaultParser
109
     * @covers SqlParserClass::tokenizer
110
     * @covers SqlParserClass::mutators
111
     */
112
    public function shouldProvideDefaultParser(): void
113
    {
114
        /** @var SqlParserClass $defaultParser */
115
        $defaultParser = SqlParserClass::defaultParser();
116
117
        $this->assertEquals(SqlTokenizerClass::defaultTokenizer(), $defaultParser->tokenizer());
118
        $this->assertEquals(SqlParserClass::defaultMutators(), $defaultParser->mutators());
119
    }
120
121
    /**
122
     * @test
123
     * @dataProvider dataProvider
124
     * @covers SqlParserClass::parseSql
125
     */
126
    public function shouldBuildCorrectAst(
127
        string $astFile,
0 ignored issues
show
Unused Code introduced by
The parameter $astFile is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

127
        /** @scrutinizer ignore-unused */ string $astFile,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
128
        string $sql,
129
        string $expectedDump
130
    ): void {
131
132
        /** @var array<string> $dumpLines */
133
        $dumpLines = explode("\n", $expectedDump);
0 ignored issues
show
Unused Code introduced by
The assignment to $dumpLines is dead and can be removed.
Loading history...
134
135
        /** @var SqlParser $parser */
136
        $parser = SqlParserClass::defaultParser();
137
138
        try {
139
            /** @var array<SqlAstNode> $detectedContent */
140
            $detectedContent = $parser->parseSql($sql);
141
142
        } catch (UnparsableSqlException $exception) {
143
            echo $exception->asciiLocationDump();
144
            
145
            throw $exception;
146
        }
147
        
148
        /** @var string $actualDump */
149
        $actualDump = $this->dumpNodes($detectedContent);
150
        
151
        if ($expectedDump !== $actualDump) {
152
            #file_put_contents('/tmp/ga_debug.ast', $this->dumpNodes($detectedContent, 0, false));
153
        }
154
155
        $this->assertEquals($expectedDump, $actualDump);
156
    }
157
    
158
    /** @return array<string, array{0:string, 1:string}> */
159
    public function dataProvider(): array
160
    {
161
        /** @var array<string> $sqlFiles */
162
        $sqlFiles = glob(sprintf('%s/%s/*.sql', __DIR__, self::DATA_FOLDER_NAME));
163
164
        /** @var array<string, array{0:string, 1:string}> $dataSets */
165
        $dataSets = array();
166
167
        /** @var string $sqlFile */
168
        foreach ($sqlFiles as $sqlFile) {
169
170
            /** @var string $astFile */
171
            $astFile = $sqlFile . '.ast';
172
173
            if (file_exists($astFile)) {
174
                $dataSets[basename($astFile)] = [
175
                    realpath($astFile),
176
                    (string) file_get_contents($sqlFile),
177
                    trim((string) file_get_contents($astFile)),
178
                ];
179
            }
180
        }
181
182
        return $dataSets;
183
    }
184
185
    /** @param array<SqlAstNode> $nodes */
186
    private function dumpNodes(array $nodes, int $level = 0, bool $withSql = false): string
187
    {
188
        /** @var array<string> $dumpLines */
189
        $dumpLines = array();
190
        
191
        /** @var SqlAstNode $node */
192
        foreach ($nodes as $node) {
193
            /** @var string $line */
194
            $line = str_pad('', $level, '-') . array_reverse(explode("\\", get_class($node)))[0];
195
            
196
            if ($withSql) {
197
                $line .= ':' . $node->toSql();
198
            }
199
            
200
            $dumpLines[] = $line;
201
            
202
            /** @var array<SqlAstNode> $children */
203
            $children = $node->children();
204
            
205
            if (!empty($children)) {
206
                $dumpLines[] = $this->dumpNodes($children, $level + 1);
207
            }
208
        }
209
        
210
        return implode("\n", $dumpLines);
211
    }
212
213
}
214