SyncDbDataMysqlTest   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 342
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 342
rs 10
c 0
b 0
f 0
wmc 14
lcom 1
cbo 6

11 Methods

Rating   Name   Duplication   Size   Complexity  
A generateUuid() 0 4 1
A removeDestTable() 0 6 2
A setUpBeforeClass() 0 8 1
A tearDownAfterClass() 0 11 1
A testLock() 0 8 1
A testSetDb() 0 15 1
A testSyncOneWayWithSourceHasNoTimestampColumn() 0 9 1
B testSyncOneWay() 0 118 2
B testSyncDelete() 0 85 2
A testSyncDeleteWithoutCompareMethod() 0 14 1
A testLockException() 0 6 1
1
<?php
2
namespace FwlibTest\Db;
3
4
use Fwlib\Db\SyncDbData;
5
use Fwlib\Test\AbstractDbRelateTest;
6
use Fwlib\Util\UtilContainer;
7
8
/**
9
 * @copyright   Copyright 2013-2015 Fwolf
10
 * @license     http://www.gnu.org/licenses/lgpl.html LGPL-3.0+
11
 */
12
class SyncDbDataMysqlTest extends AbstractDbRelateTest
13
{
14
    public static $flock = true;
15
    public static $fopen = null;
16
    public static $unlink = null;
17
18
    public static $dbUsing = 'mysql';
19
    public static $tableUserDest = 'test_user_dest';
20
21
    /** @var UtilContainer */
22
    protected static $utilContainer;
23
24
    /**
25
     * Rows of initial test data
26
     */
27
    private $totalRows = 16;
28
29
30
    protected static function generateUuid()
31
    {
32
        return self::$utilContainer->getUuidBase36()->generate();
33
    }
34
35
36
    private static function removeDestTable()
37
    {
38
        if (self::$dbMysql->isTableExist(self::$tableUserDest)) {
39
            self::$dbMysql->Execute('DROP TABLE ' . self::$tableUserDest);
40
        }
41
    }
42
43
44
    public static function setUpBeforeClass()
45
    {
46
        parent::setUpBeforeClass();
47
48
        self::removeDestTable();
49
50
        self::$utilContainer = UtilContainer::getInstance();
51
    }
52
53
54
    public static function tearDownAfterClass()
55
    {
56
        parent::tearDownAfterClass();
57
58
        // Delete record table
59
        self::$flock = true;
60
        $sdd = new SyncDbData();
61
        self::$dbMysql->Execute("DROP TABLE {$sdd->tableRecord}");
62
63
        self::removeDestTable();
64
    }
65
66
67
    public function testLock()
68
    {
69
        // Normal lock file release
70
        $sdd = new SyncDbData();
71
        $y = self::$fopen;
72
        unset($sdd);
73
        $this->assertEquals($y, self::$unlink);
74
    }
75
76
77
    public function testSetDb()
78
    {
79
        $sdd = new SyncDbData();
80
        $sdd->setDb(self::$dbMysql, self::$dbMysql->profile);
81
82
        $this->assertTrue(self::$dbMysql->isTableExist($sdd->tableRecord));
83
84
        // Create again and check record table exists
85
        $sdd = new syncDbData();
86
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
87
        $this->assertEquals(
88
            "Record table {$sdd->tableRecord} already exists.",
89
            array_pop($sdd->logMessage)
90
        );
91
    }
92
93
94
    /**
95
     * Need before testSyncOneWay(), which create timestamp column
96
     *
97
     * @expectedException Exception
98
     * @expectedExceptionMessage Table test_user in source db hasn't timestamp column.
99
     */
100
    public function testSyncOneWayWithSourceHasNoTimestampColumn()
101
    {
102
        $sdd = new SyncDbData;
103
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
104
        $config = [
105
            self::$tableUser => self::$tableUserDest,
106
        ];
107
        $sdd->syncOneWay($config);
108
    }
109
110
111
    public function testSyncOneWay()
112
    {
113
        $tableUser = self::$tableUser;
114
        $tableUserDest = self::$tableUserDest;
115
        $tableNotExist = 'test_not_exist';
116
117
118
        // Add timestamp column in source db
119
        self::$dbMysql->Execute(
120
            "ALTER TABLE {$tableUser}
121
                ADD COLUMN ts TIMESTAMP NOT NULL
122
                    DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
123
            ;"
124
        );
125
        // Refresh cached meta data
126
        self::$dbMysql->getMetaColumn($tableUser, true);
127
128
129
        // Create another test user table as sync dest
130
        self::$dbMysql->Execute(
131
            "CREATE TABLE {$tableUserDest} LIKE {$tableUser};"
132
        );
133
134
135
        // Prepare dummy date in source table
136
        $data = [];
137
        for ($i = 0; $i < $this->totalRows; $i ++) {
138
            $data[] = [
139
                'uuid'  => self::generateUuid(),
140
                'title' => "Title - $i",
141
                'age'   => $i + 20,
142
                'credit'    => $i * 100,
143
            ];
144
        }
145
        self::$dbMysql->write($tableUser, $data, 'I');
146
        $rowsSource = self::$dbMysql->getRowCount($tableUser);
0 ignored issues
show
Unused Code introduced by
$rowsSource is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
147
148
149
        // Prepare SyncDbData instance
150
        // The table $tableNotExist if test dummy, we will use mock to create
151
        // convertDataXxx() method for it, and return empty convert result to
152
        // skip data write to it.
153
        $config = [
154
            $tableUser => [$tableUserDest, $tableNotExist],
155
        ];
156
157
        $stringUtil = self::$utilContainer->getString();
158
159
        // Mock instance with 2 additional convert method
160
        $convertForNotExist = 'convertData' .
161
            $stringUtil->toStudlyCaps($tableUser) .
162
            'To' . $stringUtil->toStudlyCaps($tableNotExist);
163
        $convertForUserDest = 'convertData' .
164
            $stringUtil->toStudlyCaps($tableUser) .
165
            'To' . $stringUtil->toStudlyCaps($tableUserDest);
166
        $sdd = $this->getMock(
167
            SyncDbData::class,
168
            [$convertForNotExist]
169
        );
170
        $sdd->expects($this->any())
171
            ->method($convertForNotExist)
172
            ->will($this->returnValue(null));
173
174
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
175
        $sdd->batchSize = 10;
176
177
178
        // First sync round, limit by batchSize
179
180
        $this->assertEquals($sdd->batchSize, $sdd->syncOneway($config));
181
        $this->assertEquals(
182
            $sdd->batchSize,
183
            self::$dbMysql->getRowCount($tableUserDest)
184
        );
185
186
        // Run sync again will sync nothing, because batchSize limit
187
        $this->assertEquals(0, $sdd->syncOneWay($config));
188
189
190
        // Second sync round, full sync, not reach batchSize limit
191
192
        $sdd = $this->getMock(
193
            SyncDbData::class,
194
            [$convertForNotExist, $convertForUserDest]
195
        );
196
        $sdd->expects($this->any())
197
            ->method($convertForNotExist)
198
            ->will($this->returnValue(null));
199
200
        // Change age column through convert data method
201
        $callback = function ($sourceAr) {
202
            $sourceAr['age'] = 42;
203
            return $sourceAr;
204
        };
205
        $sdd->expects($this->any())
206
            ->method($convertForUserDest)
207
            ->will($this->returnCallback($callback));
208
209
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
210
        // Mysql timestamp is not unique, so we need raise batchSize to sync
211
        // all rows. It need not clear record table.
212
        $sdd->batchSize = 200;
213
        //self::$dbMysql->Execute('TRUNCATE TABLE ' . self::$tableUserDest);
214
215
        $this->assertEquals($this->totalRows, $sdd->syncOneWay($config));
216
        $this->assertEquals(
217
            $this->totalRows,
218
            self::$dbMysql->getRowCount($tableUserDest)
219
        );
220
221
        // In dest db, column age in all rows are same value return by
222
        // callback function we defined, assert it now.
223
        $rs = self::$dbMysql->execute(
224
            "SELECT DISTINCT age from $tableUserDest"
225
        );
226
        $this->assertEquals(1, $rs->RowCount());
227
        $this->assertEquals(42, $rs->fields['age']);
228
    }
229
230
231
    /**
232
     * Need after testSyncOneWay()
233
     */
234
    public function testSyncDelete()
235
    {
236
        $tableUser = self::$tableUser;
237
        $tableUserDest = self::$tableUserDest;
238
239
240
        // Prepare SyncDbData instance
241
        // The 2nd $tableUserDest is test dummy for empty table or table need
242
        // not to sync. All actual sync is done on 1st $tableUserDest.
243
        $config = [
244
            $tableUser => [$tableUserDest, $tableUserDest],
245
        ];
246
247
        $stringUtil = self::$utilContainer->getString();
248
249
        // Mock instance with 2 additional convert method
250
        $compareForUserDest = 'compareData' .
251
            $stringUtil->toStudlyCaps($tableUser) .
252
            'To' . $stringUtil->toStudlyCaps($tableUserDest);
253
        $sdd = $this->getMock(
254
            SyncDbData::class,
255
            [$compareForUserDest]
256
        );
257
        $db = self::$dbMysql;
258
        $callback = function () use ($db, $tableUser, $tableUserDest) {
259
            $countSource = $db->getRowCount($tableUser);
260
            $countDest = $db->getRowCount($tableUserDest);
261
262
            if (1 >= ($countDest - $countSource)) {
263
                return null;
264
265
            } else {
266
                $rs = $db->SelectLimit(
0 ignored issues
show
Documentation Bug introduced by
The method SelectLimit does not exist on object<Fwlib\Bridge\Adodb>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
267
                    "SELECT uuid FROM $tableUserDest",
268
                    $countDest - $countSource
269
                );
270
                return $rs->GetArray();
271
            }
272
        };
273
        $sdd->expects($this->any())
274
            ->method($compareForUserDest)
275
            ->will($this->returnCallback($callback));
276
277
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
278
279
280
        // If previous reach batchSize, will skip sync
281
        $sdd->batchSize = 0;
282
        $this->assertEquals(0, $sdd->syncDelete($config));
283
284
285
        $sdd->batchSize = 10;
286
287
        // First sync round, db rows count diff but still no rows need to be delete
288
        self::$dbMysql->Execute("DELETE FROM $tableUser LIMIT 1");
289
        $this->assertEquals(0, $sdd->syncDelete($config));
290
291
292
        // Second sync round, only 2 row need to delete
293
        self::$dbMysql->Execute("DELETE FROM $tableUser LIMIT 1");
294
        $this->assertEquals(2, $sdd->syncDelete($config));
295
296
297
        // Third sync round, all rest need sync but limit by batchSize
298
        // Remove 2nd $tableUserDest in $config, because do sync on it will
299
        // exceed batchSize.
300
        $config = [
301
            $tableUser => [$tableUserDest],
302
            'not_exist' => 'not_exist',
303
        ];
304
305
        self::$dbMysql->Execute("TRUNCATE TABLE $tableUser");
306
        $this->assertEquals($sdd->batchSize - 2, $sdd->syncDelete($config));
307
308
        $this->assertEquals(
309
            $this->totalRows - $sdd->batchSize,
310
            self::$dbMysql->getRowCount($tableUserDest)
311
        );
312
313
        // Sync for not_exist is skipped
314
        $this->assertEquals(
315
            "Reach batchSize limit {$sdd->batchSize}.",
316
            $sdd->logMessage[count($sdd->logMessage) - 2]
317
        );
318
    }
319
320
321
    /**
322
     * @expectedException Exception
323
     * @expectedExceptionMessage Compare method needed:
324
     */
325
    public function testSyncDeleteWithoutCompareMethod()
326
    {
327
        $tableUser = self::$tableUser;
328
        $tableUserDest = self::$tableUserDest;
329
330
        $config = [
331
            $tableUser => [$tableUserDest],
332
        ];
333
334
        $sdd = new SyncDbData;
335
        $sdd->setDb(self::$dbMysql, self::$dbMysql);
336
337
        $sdd->syncDelete($config);
338
    }
339
340
341
    /**
342
     * Put last avoid influence other tests
343
     *
344
     * @expectedException Exception
345
     * @expectedExceptionMessage Aborted: Lock file check failed.
346
     */
347
    public function testLockException()
348
    {
349
        // Duplicate instance will throw exception
350
        self::$flock = false;
351
        $sdd = new SyncDbData();
0 ignored issues
show
Unused Code introduced by
$sdd is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
352
    }
353
}
354
355
356
// Overwrite build-in function for test
357
358
namespace Fwlib\Db;
359
360
function fclose($fileHandle)
0 ignored issues
show
Unused Code introduced by
The parameter $fileHandle is not used and could be removed.

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

Loading history...
361
{
362
}
363
364
function flock($fileHandle, $mode)
0 ignored issues
show
Unused Code introduced by
The parameter $fileHandle is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $mode is not used and could be removed.

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

Loading history...
365
{
366
    return \FwlibTest\Db\SyncDbDataMysqlTest::$flock;
367
}
368
369
function fopen($path)
370
{
371
    \FwlibTest\Db\SyncDbDataMysqlTest::$fopen = $path;
372
    return $path;
373
}
374
375
function unlink($path)
376
{
377
    \FwlibTest\Db\SyncDbDataMysqlTest::$unlink = $path;
378
}
379