xref: /dokuwiki/_test/tests/ChangeLog/MediaChangeLogTest.php (revision 5a285deb52b56fa567c6d217b5c396330c58f297)
1*5a285debSAndreas Gohr<?php
2*5a285debSAndreas Gohr
3*5a285debSAndreas Gohrnamespace dokuwiki\test\ChangeLog;
4*5a285debSAndreas Gohr
5*5a285debSAndreas Gohruse dokuwiki\ChangeLog\MediaChangeLog;
6*5a285debSAndreas Gohr
7*5a285debSAndreas Gohr/**
8*5a285debSAndreas Gohr * Tests for dokuwiki\ChangeLog\MediaChangeLog.
9*5a285debSAndreas Gohr *
10*5a285debSAndreas Gohr * Media files differ from pages in one way that matters to external-change detection: media
11*5a285debSAndreas Gohr * never archives its current revision (only the previous content is copied to the attic on
12*5a285debSAndreas Gohr * replace or delete). So the attic copy of the current revision is always missing, and the
13*5a285debSAndreas Gohr * base-class logic that reads it - the unchanged-content guard and the old-size lookup for
14*5a285debSAndreas Gohr * the size change - has to be handled specially for media.
15*5a285debSAndreas Gohr */
16*5a285debSAndreas Gohrclass MediaChangeLogTest extends \DokuWikiTest
17*5a285debSAndreas Gohr{
18*5a285debSAndreas Gohr    /**
19*5a285debSAndreas Gohr     * Write a media file with the given content and mtime and record a matching changelog entry,
20*5a285debSAndreas Gohr     * leaving file mtime and changelog in agreement (as a real DokuWiki upload does).
21*5a285debSAndreas Gohr     */
22*5a285debSAndreas Gohr    protected function seedMedia($image, $content, $date, $type = DOKU_CHANGE_TYPE_CREATE, $sizechange = null)
23*5a285debSAndreas Gohr    {
24*5a285debSAndreas Gohr        io_saveFile(mediaFN($image), $content);
25*5a285debSAndreas Gohr        touch(mediaFN($image), $date);
26*5a285debSAndreas Gohr        clearstatcache();
27*5a285debSAndreas Gohr        addMediaLogEntry(
28*5a285debSAndreas Gohr            $date,
29*5a285debSAndreas Gohr            $image,
30*5a285debSAndreas Gohr            $type,
31*5a285debSAndreas Gohr            $type === DOKU_CHANGE_TYPE_CREATE ? 'created' : 'edited',
32*5a285debSAndreas Gohr            '',
33*5a285debSAndreas Gohr            null,
34*5a285debSAndreas Gohr            $sizechange === null ? strlen($content) : $sizechange
35*5a285debSAndreas Gohr        );
36*5a285debSAndreas Gohr    }
37*5a285debSAndreas Gohr
38*5a285debSAndreas Gohr    /**
39*5a285debSAndreas Gohr     * A media file whose mtime is bumped without any content change (a touch, an rsync --times,
40*5a285debSAndreas Gohr     * an unzip of identical bytes) must not be recorded as an external edit. The current media
41*5a285debSAndreas Gohr     * revision is never archived, so there is no copy to compare against; the recorded size is
42*5a285debSAndreas Gohr     * used instead, and an unchanged size means the content did not change: the mtime is reset
43*5a285debSAndreas Gohr     * to the recorded revision date and no changelog entry is added.
44*5a285debSAndreas Gohr     */
45*5a285debSAndreas Gohr    public function testTouchedMediaWithUnchangedContentIsNotExternalEdit()
46*5a285debSAndreas Gohr    {
47*5a285debSAndreas Gohr        $image = 'changelog:touched.png';
48*5a285debSAndreas Gohr        $lastRev = time() - 1000;
49*5a285debSAndreas Gohr        $this->seedMedia($image, str_repeat('x', 100), $lastRev);
50*5a285debSAndreas Gohr
51*5a285debSAndreas Gohr        // bump the file mtime forward without changing the content
52*5a285debSAndreas Gohr        touch(mediaFN($image), $lastRev + 500);
53*5a285debSAndreas Gohr        clearstatcache();
54*5a285debSAndreas Gohr
55*5a285debSAndreas Gohr        $changelog = new MediaChangeLog($image);
56*5a285debSAndreas Gohr        $currentRev = $changelog->currentRevision();
57*5a285debSAndreas Gohr        $currentInfo = $changelog->getRevisionInfo($currentRev);
58*5a285debSAndreas Gohr
59*5a285debSAndreas Gohr        $this->assertEquals($lastRev, $currentRev, 'unchanged content must not create an external revision');
60*5a285debSAndreas Gohr        $this->assertArrayNotHasKey('timestamp', $currentInfo, 'should not be a synthesized external edit');
61*5a285debSAndreas Gohr        $this->assertCount(1, $changelog->getRevisions(-1, 200), 'no external edit entry should be added');
62*5a285debSAndreas Gohr        $this->assertFileDoesNotExist(mediaFN($image, $lastRev + 500), 'no attic snapshot should be written');
63*5a285debSAndreas Gohr
64*5a285debSAndreas Gohr        clearstatcache();
65*5a285debSAndreas Gohr        $this->assertEquals($lastRev, filemtime(mediaFN($image)), 'file mtime should be reset to the changelog date');
66*5a285debSAndreas Gohr    }
67*5a285debSAndreas Gohr
68*5a285debSAndreas Gohr    /**
69*5a285debSAndreas Gohr     * A media file genuinely replaced outside DokuWiki (different content, newer mtime) is an
70*5a285debSAndreas Gohr     * external edit. Its size change must be computed against the recorded size of the previous
71*5a285debSAndreas Gohr     * revision, not against the missing attic copy of the current revision (which would report a
72*5a285debSAndreas Gohr     * zero old size and therefore the whole new file as the change). The new content is not
73*5a285debSAndreas Gohr     * snapshotted to the attic: media never archives its current revision.
74*5a285debSAndreas Gohr     */
75*5a285debSAndreas Gohr    public function testExternallyReplacedMediaRecordsCorrectSizeChange()
76*5a285debSAndreas Gohr    {
77*5a285debSAndreas Gohr        $image = 'changelog:replaced.png';
78*5a285debSAndreas Gohr        $lastRev = time() - 1000;
79*5a285debSAndreas Gohr        $this->seedMedia($image, str_repeat('a', 100), $lastRev);
80*5a285debSAndreas Gohr
81*5a285debSAndreas Gohr        // replace the file externally with a differently-sized one, newer mtime
82*5a285debSAndreas Gohr        $extRev = $lastRev + 500;
83*5a285debSAndreas Gohr        io_saveFile(mediaFN($image), str_repeat('b', 250));
84*5a285debSAndreas Gohr        touch(mediaFN($image), $extRev);
85*5a285debSAndreas Gohr        clearstatcache();
86*5a285debSAndreas Gohr
87*5a285debSAndreas Gohr        $changelog = new MediaChangeLog($image);
88*5a285debSAndreas Gohr        $currentRev = $changelog->currentRevision();
89*5a285debSAndreas Gohr        $currentInfo = $changelog->getRevisionInfo($currentRev);
90*5a285debSAndreas Gohr
91*5a285debSAndreas Gohr        $this->assertEquals($extRev, $currentRev, 'external edit detected at the file mtime');
92*5a285debSAndreas Gohr        $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type'], 'should be an external edit');
93*5a285debSAndreas Gohr        $this->assertEquals(
94*5a285debSAndreas Gohr            250 - 100,
95*5a285debSAndreas Gohr            $currentInfo['sizechange'],
96*5a285debSAndreas Gohr            'the old size must come from the recorded revision, not zero against the missing attic'
97*5a285debSAndreas Gohr        );
98*5a285debSAndreas Gohr        $this->assertFileDoesNotExist(
99*5a285debSAndreas Gohr            mediaFN($image, $extRev),
100*5a285debSAndreas Gohr            'the current revision is not archived, so the external edit is not snapshotted either'
101*5a285debSAndreas Gohr        );
102*5a285debSAndreas Gohr    }
103*5a285debSAndreas Gohr
104*5a285debSAndreas Gohr    /**
105*5a285debSAndreas Gohr     * With more than one recorded revision the old size is reconstructed from the previous
106*5a285debSAndreas Gohr     * revision's real attic copy plus the size change logged for the current revision, so a
107*5a285debSAndreas Gohr     * multi-revision media file still yields the correct external-edit size change.
108*5a285debSAndreas Gohr     */
109*5a285debSAndreas Gohr    public function testExternalEditReconstructsOldSizeFromPreviousAttic()
110*5a285debSAndreas Gohr    {
111*5a285debSAndreas Gohr        $image = 'changelog:multi.png';
112*5a285debSAndreas Gohr        $rev1 = time() - 2000;
113*5a285debSAndreas Gohr        $this->seedMedia($image, str_repeat('a', 100), $rev1);
114*5a285debSAndreas Gohr
115*5a285debSAndreas Gohr        // simulate a DokuWiki replace: archive the current revision, write new content, log the edit
116*5a285debSAndreas Gohr        $rev2 = $rev1 + 500;
117*5a285debSAndreas Gohr        io_makeFileDir(mediaFN($image, $rev1));
118*5a285debSAndreas Gohr        copy(mediaFN($image), mediaFN($image, $rev1)); // what media_saveOldRevision() does
119*5a285debSAndreas Gohr        $this->seedMedia($image, str_repeat('b', 300), $rev2, DOKU_CHANGE_TYPE_EDIT, 300 - 100);
120*5a285debSAndreas Gohr
121*5a285debSAndreas Gohr        // now replace the current (unarchived) revision externally
122*5a285debSAndreas Gohr        $rev3 = $rev2 + 500;
123*5a285debSAndreas Gohr        io_saveFile(mediaFN($image), str_repeat('c', 120));
124*5a285debSAndreas Gohr        touch(mediaFN($image), $rev3);
125*5a285debSAndreas Gohr        clearstatcache();
126*5a285debSAndreas Gohr
127*5a285debSAndreas Gohr        $changelog = new MediaChangeLog($image);
128*5a285debSAndreas Gohr        $currentRev = $changelog->currentRevision();
129*5a285debSAndreas Gohr        $currentInfo = $changelog->getRevisionInfo($currentRev);
130*5a285debSAndreas Gohr
131*5a285debSAndreas Gohr        $this->assertEquals($rev3, $currentRev);
132*5a285debSAndreas Gohr        $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type']);
133*5a285debSAndreas Gohr        $this->assertEquals(
134*5a285debSAndreas Gohr            120 - 300,
135*5a285debSAndreas Gohr            $currentInfo['sizechange'],
136*5a285debSAndreas Gohr            'old size reconstructed from the previous attic (300) plus the logged change'
137*5a285debSAndreas Gohr        );
138*5a285debSAndreas Gohr    }
139*5a285debSAndreas Gohr
140*5a285debSAndreas Gohr    /**
141*5a285debSAndreas Gohr     * An external media edit whose mtime is older than the last revision cannot trust that
142*5a285debSAndreas Gohr     * mtime as its date, so it is dated just after the last revision. The current file's mtime
143*5a285debSAndreas Gohr     * is then repaired forward to that date (media does not archive, but the base class still
144*5a285debSAndreas Gohr     * fixes the mtime) so the same change is not re-detected on the next read.
145*5a285debSAndreas Gohr     */
146*5a285debSAndreas Gohr    public function testExternalEditWithOlderMtimeIsRepairedAndStable()
147*5a285debSAndreas Gohr    {
148*5a285debSAndreas Gohr        $image = 'changelog:oldmtime.png';
149*5a285debSAndreas Gohr        $lastRev = time() - 1000;
150*5a285debSAndreas Gohr        $this->seedMedia($image, str_repeat('a', 100), $lastRev);
151*5a285debSAndreas Gohr
152*5a285debSAndreas Gohr        // replace externally with different content but an mtime older than the last revision
153*5a285debSAndreas Gohr        io_saveFile(mediaFN($image), str_repeat('b', 250));
154*5a285debSAndreas Gohr        touch(mediaFN($image), $lastRev - 100);
155*5a285debSAndreas Gohr        clearstatcache();
156*5a285debSAndreas Gohr
157*5a285debSAndreas Gohr        $this->expectLogMessage('current file modification time is older than last revision date');
158*5a285debSAndreas Gohr
159*5a285debSAndreas Gohr        $changelog = new MediaChangeLog($image);
160*5a285debSAndreas Gohr        $currentRev = $changelog->currentRevision();
161*5a285debSAndreas Gohr        $currentInfo = $changelog->getRevisionInfo($currentRev);
162*5a285debSAndreas Gohr
163*5a285debSAndreas Gohr        $this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $currentInfo['type']);
164*5a285debSAndreas Gohr        $this->assertFalse($currentInfo['timestamp'], 'an older-than-last mtime cannot be trusted as the date');
165*5a285debSAndreas Gohr        $this->assertEquals($lastRev + 1, $currentRev, 'the edit is dated just after the last revision');
166*5a285debSAndreas Gohr
167*5a285debSAndreas Gohr        // the file mtime is repaired forward, so a fresh read does not re-detect the change
168*5a285debSAndreas Gohr        clearstatcache();
169*5a285debSAndreas Gohr        $this->assertEquals($lastRev + 1, filemtime(mediaFN($image)), 'file mtime repaired to the synthesized date');
170*5a285debSAndreas Gohr        $fresh = new MediaChangeLog($image);
171*5a285debSAndreas Gohr        $this->assertEquals($lastRev + 1, $fresh->currentRevision(), 'no re-detection on the next read');
172*5a285debSAndreas Gohr        $this->assertCount(2, $fresh->getRevisions(-1, 200), 'exactly one external edit was recorded');
173*5a285debSAndreas Gohr    }
174*5a285debSAndreas Gohr}
175