├── .gitignore
├── .travis.yml
├── LICENSE.txt
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Exception
│ └── PeerNotReachableException.php
├── README
├── Replication.php
├── ReplicationTask.php
└── Replicator.php
└── tests
├── Functional
└── ReplicatorTest.php
├── ReplicationTest.php
├── ReplicatorFunctionalTestBase.php
└── TestUtil.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .idea
3 | nbproject
4 | vendor
5 | composer.lock
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | services:
4 | - couchdb
5 |
6 | php:
7 | - 5.6
8 | - 7.0
9 | - 7.1
10 | - 7.2
11 |
12 | notifications:
13 | slack:
14 | rooms:
15 | - det:YiOiwfzUBtt9aTnWvocK8uDI
16 | on_success: change
17 | on_failure: always
18 | on_start: never
19 |
20 | before_install:
21 | - composer selfupdate
22 |
23 | install:
24 | - composer install
25 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/relaxedws/couchdb-replicator)
2 |
3 | # couchdb-replicator
4 | CouchDB Replicator implemented with PHP
5 |
6 | ## Example usage
7 | ```php
8 | require __DIR__ . '/vendor/autoload.php';
9 |
10 | use Doctrine\CouchDB\CouchDBClient;
11 | use Relaxed\Replicator\ReplicationTask;
12 | use Relaxed\Replicator\Replicator;
13 |
14 | $source = CouchDBClient::create(['dbname' => 'source']);
15 | $target = CouchDBClient::create(['dbname' => 'target']);
16 |
17 | $task = new ReplicationTask();
18 | $replicator = new Replicator($source, $target, $task);
19 |
20 | $response = $replicator->startReplication();
21 | ```
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "relaxedws/replicator",
3 | "description": "couchdb-replicator",
4 | "license": "GPL-2.0+",
5 | "authors": [
6 | {
7 | "name": "abhishek kumar",
8 | "email": "abhi170893@gmail.com"
9 | }
10 | ],
11 | "require-dev": {
12 | "phpunit/phpunit": "4.8.*"
13 | },
14 | "require": {
15 | "relaxedws/couchdb": "~2"
16 | },
17 | "autoload": {
18 | "psr-4": {
19 | "Relaxed\\Replicator\\": "src"
20 | }
21 | },
22 | "autoload-dev": {
23 | "psr-4": {
24 | "Relaxed\\Replicator\\Test\\": "tests"
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
12 |
14 |
15 |
16 |
17 |
18 | tests
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Exception/PeerNotReachableException.php:
--------------------------------------------------------------------------------
1 | source = $source;
53 | $this->target = $target;
54 | $this->task = $task;
55 | }
56 |
57 | /**
58 | * Starts the replication process. $printStatus can be used to print the
59 | * status of the continuous replication to the STDOUT. The $getFinalReport
60 | * can be used to enable/disable returning of an array containing the
61 | * replication report in case of continuous replication. This is useful
62 | * when there are large number of documents. So when the replication is
63 | * continuous,to see the status set $printStatus to true and $getFinalReport
64 | * to false.
65 | *
66 | * @param bool $printStatus
67 | * @param bool $getFinalReport
68 | * @return array
69 | * @throws \Doctrine\CouchDB\HTTP\HTTPException
70 | * @throws \Exception
71 | */
72 |
73 | public function start($printStatus = true, $getFinalReport = true)
74 | {
75 | $this->startTime = new \DateTime();
76 | // DB info (via GET /{db}) for source and target.
77 | $this->verifyPeers();
78 | $this->task->setRepId($this->generateReplicationId());
79 | // Replication log (via GET /{db}/_local/{docid}) for source and target.
80 | list($sourceLog, $targetLog) = $this->getReplicationLog();
81 |
82 | $this->task->setSinceSeq($this->compareReplicationLogs($sourceLog, $targetLog));
83 |
84 | // Main replication processing
85 | $response = $this->locateChangedDocumentsAndReplicate($printStatus, $getFinalReport);
86 |
87 | $this->endTime = new \DateTime();
88 | $replicationLog = $this->putReplicationLog($response);
89 |
90 | $this->ensureFullCommit();
91 |
92 | unset($replicationLog['_id']);
93 | unset($replicationLog['_rev']);
94 | unset($replicationLog['_revisions']);
95 |
96 | return $replicationLog;
97 | }
98 |
99 |
100 | /**
101 | * @return array
102 | * @throws HTTPException
103 | * @throws \Exception
104 | */
105 | public function verifyPeers()
106 | {
107 | $sourceInfo = null;
108 | try {
109 | $sourceInfo = $this->source->getDatabaseInfo($this->source->getDatabase());
110 | } catch (HTTPException $e) {
111 | throw new PeerNotReachableException('Source not reachable.');
112 | }
113 |
114 | $targetInfo = null;
115 | try {
116 | $targetInfo = $this->target->getDatabaseInfo($this->target->getDatabase());
117 | } catch (HTTPException $e) {
118 | if ($e->getCode() == 404 && $this->task->getCreateTarget()) {
119 | $this->target->createDatabase($this->target->getDatabase());
120 | $targetInfo = $this->target->getDatabaseInfo($this->target->getDatabase());
121 | } elseif ($e->getCode() == 404) {
122 | throw new PeerNotReachableException('Target does not exist.');
123 | } else {
124 | throw new PeerNotReachableException($e->getMessage());
125 | }
126 | }
127 | return array($sourceInfo, $targetInfo);
128 | }
129 |
130 | /**
131 | * @return string
132 | * @throws HTTPException
133 | */
134 | public function generateReplicationId()
135 | {
136 | $filterCode = '';
137 | $filter = $this->task->getFilter();
138 | $parameters = $this->task->getParameters();
139 | if ($filter != null && empty($parameters)) {
140 | if ($filter[0] !== '_') {
141 | list($designDoc, $functionName) = explode('/', $filter);
142 | $designDocName = '_design/' . $designDoc;
143 | $response = $this->source->findDocument($designDocName);
144 | if ($response->status != 200) {
145 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' . $designDocName, $response);
146 | }
147 | $filterCode = $response->body['filters'][$functionName];
148 | }
149 | }
150 | return \md5(
151 | $this->source->getUuid() .
152 | $this->source->getDatabase() .
153 | $this->target->getDatabase() .
154 | \var_export($this->task->getDocIds(), true) .
155 | ($this->task->getCreateTarget() ? '1' : '0') .
156 | ($this->task->getContinuous() ? '1' : '0') .
157 | $filter .
158 | $filterCode .
159 | $this->task->getStyle() .
160 | \var_export($this->task->getHeartbeat(), true)
161 | );
162 | }
163 |
164 | /**
165 | * @return array
166 | * @throws HTTPException
167 | * @throws \Exception
168 | */
169 | public function getReplicationLog()
170 | {
171 | $replicationDocId = '_local' . '/' . $this->task->getRepId();
172 | $sourceResponse = $this->source->findDocument($replicationDocId);
173 | $targetResponse = $this->target->findDocument($replicationDocId);
174 |
175 | if ($sourceResponse->status == 200) {
176 | $this->sourceLog = $sourceResponse->body;
177 | } elseif ($sourceResponse->status != 404) {
178 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' .$replicationDocId, $sourceResponse);
179 | }
180 | if ($targetResponse->status == 200) {
181 | $this->targetLog = $targetResponse->body;
182 | } elseif ($targetResponse->status != 404) {
183 | throw HTTPException::fromResponse('/' . $this->target->getDatabase() . '/' .$replicationDocId, $targetResponse);
184 | }
185 | return array($this->sourceLog, $this->targetLog);
186 | }
187 |
188 | /**
189 | * @param array $response
190 | * @return array
191 | * @throws \Doctrine\CouchDB\HTTP\HTTPException
192 | */
193 | public function putReplicationLog(array $response) {
194 | $sessionId = \md5((\microtime(true) * 1000000));
195 | $sourceInfo = $this->source->getDatabaseInfo($this->source->getDatabase());
196 | $data = [
197 | '_id' => '_local/' . $this->task->getRepId(),
198 | 'history' => [
199 | 'recorded_seq' => $sourceInfo['update_seq'],
200 | 'session_id' => $sessionId,
201 | 'start_time' => $this->startTime->format('D, d M Y H:i:s e'),
202 | 'end_time' => $this->endTime->format('D, d M Y H:i:s e'),
203 | ],
204 | 'replication_id_version' => 3,
205 | 'session_id' => $sessionId,
206 | 'source_last_seq' => $sourceInfo['update_seq']
207 | ];
208 |
209 | if (isset($response['doc_write_failures'])) {
210 | $data['history']['doc_write_failures'] = $response['doc_write_failures'];
211 | }
212 | if (isset($response['docs_read'])) {
213 | $data['history']['docs_read'] = $response['docs_read'];
214 | }
215 | if (isset($response['missing_checked'])) {
216 | $data['history']['missing_checked'] = $response['missing_checked'];
217 | }
218 | if (isset($response['missing_found'])) {
219 | $data['history']['missing_found'] = $response['missing_found'];
220 | }
221 | if (isset($response['start_last_seq'])) {
222 | $data['history']['start_last_seq'] = $response['start_last_seq'];
223 | }
224 | if (isset($response['end_last_seq'])) {
225 | $data['history']['end_last_seq'] = $response['end_last_seq'];
226 | }
227 | if (isset($response['docs_written'])) {
228 | $data['history']['docs_written'] = $response['docs_written'];
229 | }
230 |
231 | // Creating dedicated source and target data arrays.
232 | $sourceData = $data;
233 | $targetData = $data;
234 | // Adding _rev to data array if it was in original replication log
235 | if (isset($this->sourceLog['_rev'])) {
236 | $sourceData['_rev'] = $this->sourceLog['_rev'];
237 | }
238 | if (isset($this->targetLog['_rev'])) {
239 | $targetData['_rev'] = $this->targetLog['_rev'];
240 | }
241 |
242 | // Having to work around CouchDBClient not supporting _local.
243 | $sourceResponse = $this->source->getHttpClient()->request('PUT', '/' . $this->source->getDatabase() . '/' . $data['_id'], json_encode($sourceData));
244 | $targetResponse = $this->target->getHttpClient()->request('PUT', '/' . $this->target->getDatabase() . '/' . $data['_id'], json_encode($targetData));
245 |
246 | if ($sourceResponse->status != 201) {
247 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' . $data['_id'], $sourceResponse);
248 | }
249 |
250 | if ($targetResponse->status != 201) {
251 | throw HTTPException::fromResponse('/' . $this->target->getDatabase() . '/' . $data['_id'], $targetResponse);
252 | }
253 |
254 | return $data;
255 | }
256 |
257 | /**
258 | * @param $sourceLog
259 | * @param $targetLog
260 | * @return int|mixed
261 | */
262 | public function compareReplicationLogs(&$sourceLog, &$targetLog)
263 | {
264 | $sinceSeq = 0;
265 | if ($sourceLog == null || $targetLog == null) {
266 | $sinceSeq = $this->task->getSinceSeq();
267 | } elseif ($sourceLog['session_id'] === $targetLog['session_id']) {
268 | $sinceSeq = $sourceLog['source_last_seq'];
269 | } else {
270 | foreach ($sourceLog['history'] as &$sDoc) {
271 | $matchFound = 0;
272 | foreach ($targetLog['history'] as &$tDoc) {
273 | if ($sDoc['session_id'] === $tDoc['session_id']) {
274 | $sinceSeq = $sDoc['recorded_seq'];
275 | $matchFound = 1;
276 | break;
277 | }
278 | }
279 | unset($tDoc);
280 | if ($matchFound === 1) {
281 | break;
282 | }
283 | }
284 | unset($sDoc);
285 | }
286 | return $sinceSeq;
287 | }
288 |
289 | /**
290 | * @param $changes
291 | * @return array
292 | */
293 | public function getMapping($changes)
294 | {
295 | $rows = array();
296 | if ($this->task->getContinuous() == false) {
297 | $rows = is_array($changes['results']) ? $changes['results'] : [];
298 | } else {
299 | $arr = \explode("\n",$changes);
300 | foreach ($arr as $line) {
301 | if (\strlen($line) > 0) {
302 | $rows[] = json_decode($line, true);
303 | }
304 | }
305 |
306 | }
307 | // To be sent to target/_revs_diff.
308 | $mapping = array();
309 | foreach ($rows as $row) {
310 | $mapping[$row['id']] = array();
311 | foreach ($row['changes'] as $revision) {
312 | $mapping[$row['id']][] = $revision['rev'];
313 | }
314 | }
315 | return $mapping;
316 | }
317 |
318 | /**
319 | * When $printStatus is true, the replication details are written to the
320 | * STDOUT. When $getFinalReport is true, detailed replication report is
321 | * returned and if false, only the success and failure counts are returned.
322 | * Both $printStatus and $getFinalReport are used only when the
323 | * replication is continuous and are ignored in case of normal replication.
324 | *
325 | * @param bool $printStatus
326 | * @param bool $getFinalReport
327 | * @return array
328 | * @throws HTTPException
329 | */
330 | public function locateChangedDocumentsAndReplicate($printStatus, $getFinalReport)
331 | {
332 | $finalResponse = array(
333 | 'multipartResponse' => array(),
334 | 'bulkResponse' => array(),
335 | 'errorResponse' => array(),
336 | 'missing_found' => 0,
337 | );
338 | // Filtered changes stream is not supported. So Don't use the doc_ids
339 | // to specify the specific document ids.
340 | if ($this->task->getContinuous()) {
341 | $options = array(
342 | 'feed' => 'continuous',
343 | 'style' => $this->task->getStyle(),
344 | 'heartbeat' => $this->task->getHeartbeat(),
345 | 'since' => $this->task->getSinceSeq(),
346 | 'filter' => $this->task->getFilter(),
347 | 'parameters' => $this->task->getParameters(),
348 | //'doc_ids' => $this->task->getDocIds(), // Not supported.
349 | //'limit' => 10000 //taking large value for now, needs optimisation
350 | );
351 | if ($this->task->getHeartbeat() != null) {
352 | $options['heartbeat'] = $this->task->getHeartbeat();
353 | } else {
354 | $options['timeout'] = ($this->task->getTimeout() != null ? $this->task->getTimeout() : 10000);
355 | }
356 | $changesStream = $this->source->getChangesAsStream($options);
357 | $failureCount = 0;
358 |
359 | while (!feof($changesStream)) {
360 | $changes = fgets($changesStream);
361 | if ($changes == false || trim($changes) == '' || strpos($changes,'last_seq') !==false) {
362 | sleep(2);
363 | continue;
364 | }
365 | $mapping = $this->getMapping($changes);
366 | $docId = array_keys($mapping)[0];
367 | try {
368 | // getRevisionDifference throws bad request when JSON is
369 | // empty. So check before sending.
370 | $revDiff = (count($mapping) > 0 ? $this->target->getRevisionDifference($mapping) : array());
371 | $response = $this->replicateChanges($revDiff);
372 | $finalResponse['doc_write_failures'] = 0;
373 | $finalResponse['docs_written'] = 0;
374 | $finalResponse['docs_read'] = $response['docs_read'];
375 | $finalResponse['missing_checked'] = $response['missing_checked'];
376 | if (isset($changes['results'][0]['seq'])) {
377 | $finalResponse['start_last_seq'] = $changes['results'][0]['seq'];
378 | }
379 | if (isset($changes['last_seq'])) {
380 | $finalResponse['end_last_seq'] = $changes['last_seq'];
381 | }
382 | if ($getFinalReport == true) {
383 | foreach ($response['multipartResponse'] as $docID => $res) {
384 | // Add the response of posting each revision of the
385 | // doc that had attachments.
386 | foreach ($res as $singleRevisionResponse) {
387 | // An Exception.
388 | if (is_a($singleRevisionResponse, 'Exception')) {
389 | $finalResponse['errorResponse'][$docID][] = $singleRevisionResponse;
390 | } else {
391 | $finalResponse['missing_found']++;
392 | $finalResponse['multipartResponse'][$docID][] = $singleRevisionResponse;
393 | }
394 | }
395 | }
396 | foreach ($response['bulkResponse'] as $doc_post_result) {
397 | if (!empty($doc_post_result['ok'])) {
398 | $finalResponse['docs_written']++;
399 | }
400 | elseif (!empty($doc_post_result['error'])) {
401 | $finalResponse['doc_write_failures']++;
402 | }
403 | }
404 | $finalResponse['bulkResponse'] = $response['bulkResponse'];
405 | }
406 |
407 | if ($printStatus == true) {
408 | echo 'Document with id = ' . $docId . ' successfully replicated.'. "\n";
409 | }
410 |
411 | } catch (\Exception $e) {
412 | if ($getFinalReport == true) {
413 | $finalResponse['errorResponse'][$docID][] = $e;
414 | }
415 |
416 | if ($printStatus == true) {
417 | echo 'Replication of document with id = ' . $docId . ' failed with code: ' . $e->getCode() . ".\n";
418 | }
419 |
420 | $failureCount++;
421 | }
422 | }
423 | $finalResponse['failureCount'] = $failureCount;
424 | // The final response in case of continuous replication.
425 | // In case where $getFinalReport is true, response has five keys:
426 | // (i)multipartResponse, (ii) bulkResponse, (iii)errorResponse,
427 | // (iv) successCount, (v) failureCount. The errorResponse has the
428 | // responses from the failed replication attempt of docs having
429 | // attachments. To check failures related to bulk posting, the
430 | // returned status codes can be used.
431 | // When $getFinalReport is false, the returned response has only the
432 | // successCount and failureCount.
433 | return $finalResponse;
434 |
435 | } else {
436 | $revDiff = [];
437 | $since = $this->task->getSinceSeq();
438 | $style = $this->task->getStyle();
439 | while (1) {
440 | $changes = $this->source->getChanges(
441 | array(
442 | 'feed' => 'normal',
443 | 'style' => $style,
444 | 'since' => $since,
445 | 'filter' => $this->task->getFilter(),
446 | 'parameters' => $this->task->getParameters(),
447 | 'doc_ids' => $this->task->getDocIds(),
448 | 'limit' => $this->task->getLimit(),
449 | )
450 | );
451 | if (empty($changes['results']) || empty($changes['last_seq'])) {
452 | break;
453 | }
454 | $mapping = $this->getMapping($changes);
455 | $diff = count($mapping) > 0 ? $this->target->getRevisionDifference($mapping) : [];
456 | if ($style == 'all_docs') {
457 | $revDiff = array_merge_recursive($revDiff, $diff);
458 | }
459 | else {
460 | $revDiff = array_merge($revDiff, $diff);
461 | }
462 | if (!in_array($changes['last_seq'], array_column($changes['results'], 'seq'))) {
463 | break;
464 | }
465 | $since = $changes['last_seq'];
466 | }
467 |
468 | $response = $this->replicateChanges($revDiff);
469 | $finalResponse['doc_write_failures'] = 0;
470 | $finalResponse['docs_written'] = 0;
471 | $finalResponse['docs_read'] = $response['docs_read'];
472 | $finalResponse['missing_checked'] = $response['missing_checked'];
473 | if (isset($changes['results'][0]['seq'])) {
474 | $finalResponse['start_last_seq'] = $changes['results'][0]['seq'];
475 | }
476 | if (isset($changes['last_seq'])) {
477 | $finalResponse['end_last_seq'] = $changes['last_seq'];
478 | }
479 | foreach ($response['multipartResponse'] as $docID => $res) {
480 | // Add the response of posting each revision of the
481 | // doc that had attachments.
482 | foreach ($res as $singleRevisionResponse) {
483 | // An Exception.
484 | if (is_a($singleRevisionResponse, 'Exception')) {
485 | $finalResponse['errorResponse'][$docID][] = $singleRevisionResponse;
486 | } else {
487 | $finalResponse['missing_found']++;
488 | $finalResponse['multipartResponse'][$docID][] = $singleRevisionResponse;
489 | }
490 | }
491 | }
492 | foreach ($response['bulkResponse'] as $doc_post_result) {
493 | if (!empty($doc_post_result['ok'])) {
494 | $finalResponse['docs_written']++;
495 | }
496 | elseif (!empty($doc_post_result['error'])) {
497 | $finalResponse['doc_write_failures']++;
498 | }
499 | }
500 | $finalResponse['bulkResponse'] = $response['bulkResponse'];
501 | // In case of normal replication the $finalResponse has three
502 | // keys: (i) multipartResponse, (ii) bulkResponse, (iii)=
503 | // errorResponse.
504 | return $finalResponse;
505 | }
506 | }
507 |
508 | /**
509 | * @param array $revDiff
510 | * @return array
511 | * @throws HTTPException|\Exception
512 | */
513 | public function replicateChanges(array &$revDiff)
514 | {
515 | $allResponse = array(
516 | 'multipartResponse' => array(),
517 | 'bulkResponse' => array(),
518 | 'docs_read' => 0,
519 | 'missing_checked' => 0
520 | );
521 |
522 | $bulkUpdater = $this->target->createBulkUpdater();
523 | $bulkUpdater->setNewEdits(false);
524 | $bulkDocsLimit = $this->task->getBulkDocsLimit();
525 | while (!empty($revDiff)) {
526 | $processRevs = array_splice($revDiff, 0, $bulkDocsLimit);
527 | foreach ($processRevs as $docId => $revMisses) {
528 | $allResponse['docs_read']++;
529 | $allResponse['missing_checked'] += count($revMisses['missing']);
530 | try {
531 | $path = '/' . $this->source->getDatabase() . '/'. $docId;
532 | $params = ['revs' => true, 'latest' => true, 'open_revs' => json_encode($revMisses['missing'])];
533 | $query = http_build_query($params);
534 | $path .= '?' . $query;
535 | $response = $this->source->transferChangedDocuments($docId, $revMisses['missing'], $this->target);
536 | if ($response instanceof ErrorResponse) {
537 | throw HTTPException::fromResponse($path, $response);
538 | }
539 | list($docStack, $multipartResponse) = $response;
540 | } catch (\Exception $e) {
541 | throw new \Exception($e->getMessage(), $e->getCode());
542 | }
543 | $bulkUpdater->updateDocuments($docStack);
544 | // $multipartResponse is an empty array in case there was no
545 | // transferred revision that had attachment in the current doc.
546 | $allResponse['multipartResponse'][$docId] = $multipartResponse;
547 | }
548 | $allResponse['bulkResponse'] += $bulkUpdater->executeByLimit($bulkDocsLimit);
549 | $bulkUpdater->emptyDocuments();
550 | }
551 | return $allResponse;
552 | }
553 |
554 | /**
555 | * @throws \Doctrine\CouchDB\HTTP\HTTPException
556 | */
557 | public function ensureFullCommit()
558 | {
559 | $this->target->ensureFullCommit();
560 | }
561 |
562 | }
563 |
--------------------------------------------------------------------------------
/src/ReplicationTask.php:
--------------------------------------------------------------------------------
1 | repId = $repId;
105 | $this->continuous = $continuous;
106 | $this->filter = $filter;
107 | $this->parameters = $parameters;
108 | $this->createTarget = $createTarget;
109 | $this->docIds = $docIds;
110 | $this->heartbeat = $heartbeat;
111 | $this->timeout = $timeout;
112 | $this->cancel = $cancel;
113 | $this->style = $style;
114 | $this->sinceSeq = $sinceSeq;
115 | $this->limit = $limit;
116 | $this->bulkDocsLimit = $bulkDocsLimit;
117 |
118 | if ($docIds != null) {
119 | \sort($this->docIds);
120 | if ($filter == null) {
121 | $this->filter = '_doc_ids';
122 | }
123 | elseif ($filter !== '_doc_ids') {
124 | throw new \InvalidArgumentException('If docIds is specified,
125 | the filter should be set as _doc_ids');
126 | }
127 | }
128 | }
129 |
130 | /**
131 | * @return mixed
132 | */
133 | public function getRepId()
134 | {
135 | return $this->repId;
136 | }
137 |
138 | /**
139 | * @param mixed $repId
140 | */
141 | public function setRepId($repId)
142 | {
143 | $this->repId = $repId;
144 | }
145 |
146 | /**
147 | * @return bool
148 | */
149 | public function getContinuous()
150 | {
151 | return $this->continuous;
152 | }
153 |
154 | /**
155 | * @param bool $continuous
156 | */
157 | public function setContinuous($continuous)
158 | {
159 | $this->continuous = $continuous;
160 | }
161 |
162 | /**
163 | * @return string
164 | */
165 | public function getFilter()
166 | {
167 | return $this->filter;
168 | }
169 |
170 | /**
171 | * @param string $filter
172 | */
173 | public function setFilter($filter)
174 | {
175 | $this->filter = $filter;
176 | }
177 |
178 | /**
179 | * @return array
180 | */
181 | public function getParameters()
182 | {
183 | return $this->parameters;
184 | }
185 |
186 | /**
187 | * @param array|NULL $parameters
188 | * An associative array of name-value parameters.
189 | */
190 | public function setParameters(array $parameters = NULL) {
191 | if ($parameters == NULL) {
192 | $parameters = [];
193 | }
194 | $this->parameters = $parameters;
195 | }
196 |
197 | /**
198 | * @param string $name
199 | * The parameter name to set.
200 | * @param string $value
201 | * The value for the parameter.
202 | */
203 | public function setParameter($name, $value)
204 | {
205 | if (!is_array($this->parameters)) {
206 | $this->setParameters([]);
207 | }
208 | $this->parameters[$name] = $value;
209 | }
210 |
211 | /**
212 | * @param int
213 | */
214 | public function setLimit($limit)
215 | {
216 | $this->limit = $limit;
217 | }
218 |
219 | /**
220 | * @param int
221 | */
222 | public function setBulkDocsLimit($bulkDocsLimit)
223 | {
224 | $this->bulkDocsLimit = $bulkDocsLimit;
225 | }
226 |
227 | /**
228 | * @return boolean
229 | */
230 | public function getCreateTarget()
231 | {
232 | return $this->createTarget;
233 | }
234 |
235 | /**
236 | * @param boolean $createTarget
237 | */
238 | public function setCreateTarget($createTarget)
239 | {
240 | $this->createTarget = $createTarget;
241 | }
242 |
243 | /**
244 | * @return array
245 | */
246 | public function getDocIds()
247 | {
248 | return $this->docIds;
249 | }
250 |
251 | /**
252 | * @return int
253 | */
254 | public function getLimit()
255 | {
256 | return $this->limit;
257 | }
258 |
259 | /**
260 | * @return int
261 | */
262 | public function getBulkDocsLimit()
263 | {
264 | return $this->bulkDocsLimit;
265 | }
266 |
267 | /**
268 | * @param array $docIds
269 | */
270 | public function setDocIds($docIds)
271 | {
272 | if ($docIds != null) {
273 | \sort($docIds);
274 | if ($this->filter == null) {
275 | $this->filter = '_doc_ids';
276 | }
277 | elseif ($this->filter !== '_doc_ids') {
278 | throw new \InvalidArgumentException('If docIds is specified,
279 | the filter should be set as _doc_ids');
280 | }
281 | }
282 | $this->docIds = $docIds;
283 | }
284 |
285 | /**
286 | * @return int
287 | */
288 | public function getHeartbeat()
289 | {
290 | return $this->heartbeat;
291 | }
292 |
293 | /**
294 | * @param int $heartbeat
295 | */
296 | public function setHeartbeat($heartbeat)
297 | {
298 | $this->heartbeat = $heartbeat;
299 | }
300 |
301 | /**
302 | * @return int
303 | */
304 | public function getTimeout()
305 | {
306 | return $this->timeout;
307 | }
308 |
309 | /**
310 | * @param int $timeout
311 | */
312 | public function setTimeout($timeout)
313 | {
314 | $this->timeout = $timeout;
315 | }
316 |
317 | /**
318 | * @return mixed
319 | */
320 | public function getCancel()
321 | {
322 | return $this->cancel;
323 | }
324 |
325 | /**
326 | * @param mixed $cancel
327 | */
328 | public function setCancel($cancel)
329 | {
330 | $this->cancel = $cancel;
331 | }
332 |
333 | /**
334 | * @return mixed
335 | */
336 | public function getStyle()
337 | {
338 | return $this->style;
339 | }
340 |
341 | /**
342 | * @param mixed $style
343 | */
344 | public function setStyle($style)
345 | {
346 | $this->style = $style;
347 | }
348 |
349 | /**
350 | * @return mixed
351 | */
352 | public function getSinceSeq()
353 | {
354 | return $this->sinceSeq;
355 | }
356 |
357 | /**
358 | * @param mixed $sinceSeq
359 | */
360 | public function setSinceSeq($sinceSeq)
361 | {
362 | $this->sinceSeq = $sinceSeq;
363 | }
364 |
365 | }
366 |
--------------------------------------------------------------------------------
/src/Replicator.php:
--------------------------------------------------------------------------------
1 | source = $source;
39 | $this->target = $target;
40 | $this->task = $task;
41 | }
42 |
43 | /**
44 | * Start the replicator. $printStatus can be used to print the status of
45 | * the continuous replication to the STDOUT. The $getFinalReport can be
46 | * used to enable/disable returning of an array containing the
47 | * replication report in case of continuous replication.
48 | *
49 | * @param bool $printStatus
50 | * @param bool $getFinalReport
51 | * @return array
52 | */
53 | public function startReplication($printStatus = true, $getFinalReport = false)
54 | {
55 | if ($this->source == null) {
56 | throw new \UnexpectedValueException('Source is Null.');
57 | }
58 | if ($this->target == null) {
59 | throw new \UnexpectedValueException('Target is Null.');
60 | }
61 | if ($this->task == null) {
62 | throw new \UnexpectedValueException('Task is Null.');
63 | }
64 |
65 | $replication = new Replication($this->source, $this-> target, $this->task);
66 |
67 | // Start and return the details of the replication.
68 | return $replication->start($printStatus, $getFinalReport);
69 | }
70 |
71 | /**
72 | * @throws Exception
73 | */
74 | public function cancelReplication()
75 | {
76 | throw new \Exception('Not defined');
77 | }
78 |
79 | /**
80 | * @return CouchDBClient
81 | */
82 | public function getSource()
83 | {
84 | return $this->source;
85 | }
86 |
87 | /**
88 | * @param CouchDBClient $source
89 | */
90 | public function setSource(CouchDBClient $source)
91 | {
92 | $this->source = $source;
93 | }
94 |
95 | /**
96 | * @return CouchDBClient
97 | */
98 | public function getTarget()
99 | {
100 | return $this->target;
101 | }
102 |
103 | /**
104 | * @param CouchDBClient $target
105 | */
106 | public function setTarget(CouchDBClient $target)
107 | {
108 | $this->target = $target;
109 | }
110 |
111 | /**
112 | * @return ReplicationTask
113 | */
114 | public function getTask()
115 | {
116 | return $this->task;
117 | }
118 |
119 | /**
120 | * @param ReplicationTask $task
121 | */
122 | public function setTask(ReplicationTask $task)
123 | {
124 | $this->task = $task;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/tests/Functional/ReplicatorTest.php:
--------------------------------------------------------------------------------
1 | sourceClient = $this->getSourceCouchDBClient();
18 | $this->targetClient = $this->getTargetCouchDBClient();
19 | $this->replicationTask = $this->getReplicationTask();
20 | // Disable default Heartbeat and use timeout. This is to make the
21 | // connection terminate quickly when there are no changes happening on
22 | // the source in case of the continuous replication.
23 | $this->replicationTask->setHeartbeat(null);
24 | // Timeout to be used in the case of continuous replication. It's in
25 | // milliseconds.
26 | $this->replicationTask->setTimeout(100);
27 |
28 | // Create the source and the target databases.
29 | $this->sourceClient->createDatabase($this->getSourceTestDatabase());
30 | $this->sourceClient->createDatabase($this->getTargetTestDatabase());
31 |
32 | $this->replicator = new Replicator(
33 | $this->sourceClient,
34 | $this->targetClient,
35 | $this->replicationTask
36 | );
37 | }
38 |
39 |
40 | /**
41 | * @expectedException \UnexpectedValueException
42 | * @expectedExceptionMessage Source is Null.
43 | */
44 | public function testStartReplicationThrowsExceptionOnNullSource()
45 | {
46 | $this->replicator = new Replicator(
47 | null,
48 | $this->targetClient,
49 | $this->replicationTask
50 | );
51 | $this->replicator->startReplication();
52 | }
53 |
54 | /**
55 | * @expectedException \UnexpectedValueException
56 | * @expectedExceptionMessage Target is Null.
57 | */
58 | public function testStartReplicationThrowsExceptionOnNullTarget()
59 | {
60 | $this->replicator = new Replicator(
61 | $this->sourceClient,
62 | null,
63 | $this->replicationTask
64 | );
65 | $this->replicator->startReplication();
66 | }
67 |
68 | /**
69 | * @expectedException \UnexpectedValueException
70 | * @expectedExceptionMessage Task is Null.
71 | */
72 | public function testStartReplicationThrowsExceptionOnNullTask()
73 | {
74 | $this->replicator = new Replicator(
75 | $this->sourceClient,
76 | $this->targetClient,
77 | null
78 | );
79 | $this->replicator->startReplication();
80 | }
81 |
82 | /**
83 | * @expectedException \Exception
84 | * @expectedExceptionMessage Source not reachable.
85 | */
86 | public function testStartReplicationThrowsExceptionWhenSourceDoesNotExist()
87 | {
88 | // Delete the source database.
89 | $this->sourceClient->deleteDatabase($this->getSourceTestDatabase());
90 | try {
91 | $this->replicator->startReplication();
92 | } catch (\Exception $e) {
93 | // Restore state before throwing the raised exception.
94 | $this->sourceClient->createDatabase($this->getSourceTestDatabase());
95 | throw $e;
96 | }
97 |
98 | }
99 |
100 | /**
101 | * @expectedException \Exception
102 | * @expectedExceptionMessage Target does not exist.
103 | */
104 | public function testStartReplicationThrowsExceptionWhenTargetDoesNotExist()
105 | {
106 | // Delete the target database.
107 | $this->targetClient->deleteDatabase($this->getTargetTestDatabase());
108 | try {
109 | $this->replicator->startReplication();
110 | } catch (\Exception $e) {
111 | // Restore state before throwing the raised exception.
112 | $this->targetClient->createDatabase($this->getTargetTestDatabase());
113 | throw $e;
114 | }
115 |
116 | }
117 |
118 | public function testTargetCreation()
119 | {
120 | // Delete the target database.
121 | $this->targetClient->deleteDatabase($this->getTargetTestDatabase());
122 | // Enable target creation.
123 | $this->replicationTask->setCreateTarget(true);
124 | $this->replicator->setTask($this->replicationTask);
125 | // Start the replication.
126 | $this->replicator->startReplication();
127 |
128 | $data = $this->targetClient->getDatabaseInfo();
129 |
130 | // The target should have been created.
131 | $this->assertInternalType('array', $data);
132 | $this->assertArrayHasKey('db_name', $data);
133 | $this->assertEquals($this->getTargetTestDatabase(), $data['db_name']);
134 |
135 | }
136 |
137 | public function isContinuousReplicationProvider()
138 | {
139 | return array(
140 | // Normal replication.
141 | array(false),
142 | // Continuous replication.
143 | array(true)
144 | );
145 | }
146 |
147 | /**
148 | * @dataProvider isContinuousReplicationProvider
149 | */
150 | public function testReplicationWithoutAttachments($isContinuous)
151 | { // Set the replication type.
152 | $this->replicationTask->setContinuous($isContinuous);
153 |
154 | // Add three docs to the source db.
155 | for ($i = 0; $i < 3; $i++) {
156 | list($id, $rev) = $this->sourceClient->putDocument(
157 | array("foo" => "bar" . var_export($i, true)),
158 | 'id' . var_export($i, true)
159 | );
160 | }
161 | $this->replicator->startReplication(true, true);
162 | // Fetch the documents.
163 | $response = $this->targetClient->findDocuments(
164 | array('id0', 'id1', 'id2')
165 | );
166 | $this->assertInternalType('array', $response->body);
167 | $body = $response->body['rows'];
168 | $this->assertEquals(3, count($body));
169 | $this->assertArrayHasKey('id', $body[0]);
170 | $this->assertEquals('id0', $body[0]['id']);
171 | $this->assertEquals('id1', $body[1]['id']);
172 | $this->assertEquals('id2', $body[2]['id']);
173 | }
174 |
175 | public function testFilteredReplication()
176 | {
177 | // Add four docs to the source db. Replicate only id1 and id3 for
178 | // checking filtered Replication.
179 | for ($i = 1; $i <= 4; $i++) {
180 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i");
181 | }
182 | // Specify docs to be replicated. id2 and id4 should not be replicated.
183 | $this->replicationTask->setDocIds(array('id1', 'id3'));
184 | $this->replicator->setTask($this->replicationTask);
185 | $this->replicator->startReplication();
186 | $response = $this->targetClient->findDocuments(
187 | array('id1', 'id2', 'id3', 'id4')
188 | );
189 | $this->assertInternalType('array', $response->body);
190 | $body = $response->body['rows'];
191 | $this->assertEquals(4, count($body));
192 | $this->assertArrayHasKey('id', $body[0]);
193 | $this->assertEquals('id1', $body[0]['id']);
194 | $this->assertArrayHasKey('error', $body[1]);
195 | $this->assertEquals('not_found', $body[1]['error']);
196 | $this->assertEquals('id3', $body[2]['id']);
197 | $this->assertArrayHasKey('error', $body[3]);
198 | $this->assertEquals('not_found', $body[3]['error']);
199 |
200 | }
201 |
202 | public function testChangesLimitReplication()
203 | {
204 | // Replicate 9 docs.
205 | $docs_count = 9;
206 | for ($i = 1; $i <= $docs_count; $i++) {
207 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i");
208 | }
209 | // Set changes limit to 2.
210 | $this->replicationTask->setLimit(2);
211 | $this->replicator->setTask($this->replicationTask);
212 | $this->replicator->startReplication();
213 | $response = $this->targetClient->allDocs();
214 | $this->assertInternalType('array', $response->body);
215 | $body = $response->body['rows'];
216 | $this->assertEquals($docs_count, count($body));
217 | }
218 |
219 | public function testBulkDocsLimitReplication()
220 | {
221 | // Replicate 9 docs.
222 | $docs_count = 9;
223 | for ($i = 1; $i <= $docs_count; $i++) {
224 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i");
225 | }
226 | // Set BulkDocs limit to 2.
227 | $this->replicationTask->setBulkDocsLimit(2);
228 | $this->replicator->setTask($this->replicationTask);
229 | $this->replicator->startReplication();
230 | $response = $this->targetClient->allDocs();
231 | $this->assertInternalType('array', $response->body);
232 | $body = $response->body['rows'];
233 | $this->assertEquals($docs_count, count($body));
234 | }
235 |
236 | /**
237 | * @dataProvider isContinuousReplicationProvider
238 | */
239 | public function testReplicationWithAttachments($isContinuous)
240 | {
241 | // Set the replication type.
242 | $this->replicationTask->setContinuous($isContinuous);
243 | // Test replication with attachments.
244 | // Doc id.
245 | $id = 'multiple_attachments';
246 | // Document with attachments.
247 | $docWithAttachment1 = array (
248 | '_id' => $id,
249 | '_rev' => '1-abc',
250 | '_attachments' =>
251 | array (
252 | 'foo.txt' =>
253 | array (
254 | 'content_type' => 'text/plain',
255 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
256 | ),
257 | 'bar.txt' =>
258 | array (
259 | 'content_type' => 'text/plain',
260 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
261 | ),
262 | ),
263 | );
264 | // Doc without any attachment. The id of both the docs is same.
265 | // So we will get two leaf revisions.
266 | $doc = array('_id' => $id, '_rev' => '1-bcd', 'foo' => 'bar');
267 | // Another document with attachments.
268 | $docWithAttachment2 = array (
269 | '_id' => $id . '2',
270 | '_rev' => '1-lala',
271 | '_attachments' =>
272 | array (
273 | 'abhi.txt' =>
274 | array (
275 | 'content_type' => 'text/plain',
276 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
277 | ),
278 | 'dixon.txt' =>
279 | array (
280 | 'content_type' => 'text/plain',
281 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
282 | ),
283 | ),
284 | );
285 |
286 | // Add the documents to the test db using Bulk API.
287 | $updater = $this->sourceClient->createBulkUpdater();
288 | $updater->updateDocument($docWithAttachment1);
289 | $updater->updateDocument($docWithAttachment2);
290 | $updater->updateDocument($doc);
291 | // Set newedits to false to use the supplied _rev instead of assigning
292 | // new ones.
293 | $updater->setNewEdits(false);
294 | $updater->execute();
295 |
296 | // Start the replication. Print the status to STDOUT and also get the
297 | // details in an array.
298 | $repDetails = $this->replicator->startReplication(true, true);
299 |
300 | // Test the replication.
301 | // Fetch all the revisions of the first doc.
302 | $response = $this->targetClient->findRevisions($id, true);
303 | $this->assertObjectHasAttribute('body', $response);
304 | $this->assertInternalType('array', $response->body);
305 | $this->assertEquals(2, count($response->body));
306 | // Doc with _rev = 1-bcd
307 | $this->assertEquals(array('ok' => $doc), $response->body[0]);
308 | // Doc with _rev = 1-abc
309 | $this->assertEquals(3, count($response->body[1]['ok']));
310 | $this->assertEquals($id, $response->body[1]['ok']['_id']);
311 | $this->assertEquals('1-abc', $response->body[1]['ok']['_rev']);
312 | $this->assertEquals(
313 | 2,
314 | count($response->body[1]['ok']['_attachments'])
315 | );
316 | $this->assertArrayHasKey(
317 | 'foo.txt',
318 | $response->body[1]['ok']['_attachments']
319 | );
320 | $this->assertArrayHasKey(
321 | 'bar.txt',
322 | $response->body[1]['ok']['_attachments']
323 | );
324 | // Fetch the second document. This has only one revision.
325 | $response = $this->targetClient->findDocument($id . '2');
326 | $this->assertObjectHasAttribute('body', $response);
327 | $this->assertInternalType('array', $response->body);
328 | $this->assertEquals(3, count($response->body));
329 | $this->assertEquals($id . '2', $response->body['_id']);
330 | $this->assertEquals('1-lala', $response->body['_rev']);
331 | $this->assertEquals(
332 | 2,
333 | count($response->body['_attachments'])
334 | );
335 | $this->assertArrayHasKey(
336 | 'abhi.txt',
337 | $response->body['_attachments']
338 | );
339 | $this->assertArrayHasKey(
340 | 'dixon.txt',
341 | $response->body['_attachments']
342 | );
343 | }
344 |
345 | public function tearDown()
346 | {
347 | $this->sourceClient->deleteDatabase($this->getSourceTestDatabase());
348 | $this->sourceClient->deleteDatabase($this->getTargetTestDatabase());
349 | }
350 |
351 | }
--------------------------------------------------------------------------------
/tests/ReplicationTest.php:
--------------------------------------------------------------------------------
1 | source = $this->getMockBuilder('Doctrine\CouchDB\CouchDBClient')
20 | ->disableOriginalConstructor()
21 | ->getMock();
22 | $this->target = $this->getMockBuilder('Doctrine\CouchDB\CouchDBClient')
23 | ->disableOriginalConstructor()
24 | ->getMock();
25 |
26 | $this->response=new Response(200, array(), array('reason' => 'someReasonAsIAmTesting'), true);
27 | }
28 |
29 | /**
30 | * @expectedException \Exception
31 | * @expectedExceptionMessage Source not reachable.
32 | */
33 | Public function testVerifyPeersRaisesExceptionWhenSourceIsNotReachable()
34 | {
35 | $this->source->expects($this->once())
36 | ->method('getDatabase')
37 | ->willReturn('test_source_database');
38 | $this->source->expects($this->once())
39 | ->method('getDatabaseInfo')
40 | ->willThrowException(new HTTPException());
41 | $task = new ReplicationTask();
42 |
43 | $replication = new Replication($this->source, $this->target, $task);
44 | $replication->verifyPeers();
45 |
46 | }
47 |
48 | /**
49 | * @expectedException \Exception
50 | * @expectedExceptionMessage Target does not exist.
51 | */
52 | Public function testVerifyPeersRaisesExceptionWhenTargetDoesNotExistAndIsNotToBeCreated()
53 | {
54 | $this->source->expects($this->once())
55 | ->method('getDatabase')
56 | ->willReturn('test_source_database');
57 | $this->source->expects($this->once())
58 | ->method('getDatabaseInfo')
59 | ->willReturn(array(
60 | 'db_name' => 'test_source_database',
61 | 'instance_start_time' => '123',
62 | 'update_seq' => '456'
63 | ));
64 | $this->target->expects($this->once())
65 | ->method('getDatabase')
66 | ->willReturn('test_target_database');
67 | $this->response->status = 404;
68 | $this->target->expects($this->once())
69 | ->method('getDatabaseInfo')
70 | ->willThrowException(HTTPException::fromResponse(null, $this->response));
71 | $task = new ReplicationTask();
72 |
73 | $replication = new Replication($this->source, $this->target, $task);
74 | $replication->verifyPeers();
75 |
76 | }
77 |
78 | Public function testVerifyPeersWhenWhenTargetDoesNotExistAndIsToBeCreated()
79 | {
80 | $this->source->expects($this->once())
81 | ->method('getDatabase')
82 | ->willReturn('test_source_database');
83 | $this->source->expects($this->once())
84 | ->method('getDatabaseInfo')
85 | ->willReturn(array(
86 | 'db_name' => 'test_source_database',
87 | 'instance_start_time' => '123',
88 | 'update_seq' => '456'
89 | ));
90 | $this->target->expects($this->exactly(3))
91 | ->method('getDatabase')
92 | ->willReturn('test_target_database');
93 | $this->response->status = 404;
94 | $this->target->expects($this->exactly(2))
95 | ->method('getDatabaseInfo')
96 | ->will($this->onConsecutiveCalls(
97 | $this->throwException(HTTPException::fromResponse('path', $this->response)),
98 | array(
99 | 'db_name' => 'test_target_database',
100 | 'instance_start_time' => '123',
101 | 'update_seq' => '456'
102 | )));
103 | $this->target->expects($this->once())
104 | ->method('createDatabase')
105 | ->willReturn('');
106 | $task = new ReplicationTask();
107 | $task->setCreateTarget(true);
108 |
109 | $replication = new Replication($this->source, $this->target, $task);
110 | $response = $replication->verifyPeers();
111 | $this->assertEquals(\count($response), 2,
112 | 'Source and target info not correctly returned.');
113 | }
114 |
115 | Public function testVerifyPeersWhenSourceAndTargetAlreadyExist()
116 | {
117 | $this->source->expects($this->once())
118 | ->method('getDatabase')
119 | ->willReturn('test_source_database');
120 | $this->source->expects($this->once())
121 | ->method('getDatabaseInfo')
122 | ->willReturn(array(
123 | 'db_name' => 'test_source_database',
124 | 'instance_start_time' => '123',
125 | 'update_seq' => '456'
126 | ));
127 | $this->target->expects($this->once())
128 | ->method('getDatabase')
129 | ->willReturn('test_target_database');
130 | $this->target->expects($this->once())
131 | ->method('getDatabaseInfo')
132 | ->willReturn(array(
133 | 'db_name' => 'test_target_database',
134 | 'instance_start_time' => '123',
135 | 'update_seq' => '456'
136 | ));
137 | $task = new ReplicationTask();
138 |
139 | $replication = new Replication($this->source, $this->target, $task);
140 | list($sourceInfo, $targetInfo) = $replication->verifyPeers();
141 |
142 | $this->assertArrayHasKey("update_seq", $sourceInfo, 'Source info not correctly returned.');
143 | $this->assertArrayHasKey("instance_start_time", $sourceInfo, 'Source info not correctly returned.');
144 | $this->assertArrayHasKey("update_seq", $targetInfo, 'Target info not correctly returned.');
145 | $this->assertArrayHasKey("instance_start_time", $targetInfo, 'Target info not correctly returned.');
146 | }
147 |
148 | public function testGenerateReplicationId()
149 | {
150 | $this->source->expects($this->once())
151 | ->method('getDatabase')
152 | ->willReturn('test_source_database');
153 | $this->target->expects($this->once())
154 | ->method('getDatabase')
155 | ->willReturn('test_target_database');
156 | $task = new ReplicationTask();
157 | $expectedId = md5(
158 | 'test_source_database' .
159 | 'test_target_database' .
160 | \var_export(null, true) .
161 | '0' .
162 | '0' .
163 | null .
164 | null .
165 | 'all_docs' .
166 | '10000'
167 | );
168 | $replication = new Replication($this->source, $this->target, $task);
169 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.');
170 | }
171 |
172 | public function testGenerateReplicationIdWithFilter()
173 | {
174 | $filterCode = "function(doc, req) { if (doc._deleted) { return true; } if(!doc.clientId) { return false; } }";
175 | $this->source->expects($this->once())
176 | ->method('getDatabase')
177 | ->willReturn('test_source_database');
178 | $this->response->status = 200;
179 | $this->response->body = array('filters' => array('testFilterFunction' => $filterCode));
180 | $this->source->expects($this->once())
181 | ->method('findDocument')
182 | ->willReturn($this->response);
183 | $this->target->expects($this->once())
184 | ->method('getDatabase')
185 | ->willReturn('test_target_database');
186 | $task = new ReplicationTask(
187 | null,false,'test/testFilterFunction', [], true,
188 | null, 10000, 10000, false, 'all_docs', 0
189 | );
190 | $expectedId = md5(
191 | 'test_source_database' .
192 | 'test_target_database' .
193 | \var_export(null, true) .
194 | '1' .
195 | '0' .
196 | 'test/testFilterFunction' .
197 | $filterCode .
198 | 'all_docs' .
199 | '10000'
200 | );
201 | $replication = new Replication($this->source, $this->target, $task);
202 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.');
203 | }
204 |
205 | public function testGenerateReplicationIdWithDocIds()
206 | {
207 | $this->source->expects($this->once())
208 | ->method('getDatabase')
209 | ->willReturn('test_source_database');
210 | $this->target->expects($this->once())
211 | ->method('getDatabase')
212 | ->willReturn('test_target_database');
213 | $task = new ReplicationTask(
214 | null, false, '_doc_ids', [], true,
215 | array(1, 3, 2, 'jfajs57s868'),
216 | 10000, 10000, false, 'all_docs', 0
217 | );
218 | $expectedId = md5(
219 | 'test_source_database' .
220 | 'test_target_database' .
221 | \var_export(array('jfajs57s868', 1, 2, 3), true) .
222 | '1' .
223 | '0' .
224 | '_doc_ids' .
225 | '' .
226 | 'all_docs' .
227 | '10000'
228 | );
229 | $replication = new Replication($this->source, $this->target, $task);
230 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.');
231 | }
232 |
233 | public function testGetReplicationLog()
234 | {
235 | $sourceResponse = $this->response;
236 | $sourceResponse->body = array("log" => "source_replication_log");
237 | $sourceResponse->status = 200;
238 | $this->source->expects($this->once())
239 | ->method('findDocument')
240 | ->willReturn($sourceResponse);
241 |
242 | $targetResponse = clone $this->response;
243 | $targetResponse->status = 404;
244 |
245 | $this->target->expects($this->once())
246 | ->method('findDocument')
247 | ->willReturn($targetResponse);
248 |
249 | $task = new ReplicationTask();
250 | $replication = new Replication($this->source, $this->target, $task);
251 | list($sourceLog, $targetLog) = $replication->getReplicationLog();
252 | $this->assertEquals($sourceLog, array("log" => "source_replication_log"));
253 | $this->assertEquals($targetLog, null);
254 |
255 | }
256 |
257 | /**
258 | * @expectedException \Doctrine\CouchDB\HTTP\HTTPException
259 | */
260 | public function testGetReplicationLogRaisesExceptionWhenPeerNotReachable()
261 | {
262 | $this->response->status = 500;
263 | $this->source->expects($this->once())
264 | ->method('findDocument')
265 | ->willThrowException(HTTPException::fromResponse(null, $this->response));
266 |
267 | $task = new ReplicationTask();
268 | $replication = new Replication($this->source, $this->target, $task);
269 | list($sourceLog, $targetLog) = $replication->getReplicationLog();
270 | $this->assertEquals($targetLog, array("log" => "source_replication_log"));
271 | $this->assertEquals($sourceLog, null);
272 |
273 | }
274 |
275 | /**
276 | * @dataProvider replicationLogsProvider
277 | */
278 | public function testCompareReplicationLogs($sourceLog, $targetLog, $expectedSequence)
279 | {
280 | $task = new ReplicationTask();
281 | $replication = new Replication($this->source, $this->target, $task);
282 | $this->assertEquals($expectedSequence, $replication->compareReplicationLogs($sourceLog,$targetLog));
283 |
284 | }
285 |
286 | public function replicationLogsProvider()
287 | {
288 | return array(
289 | array(
290 | array (
291 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a',
292 | '_rev' => '0-8',
293 | 'history' =>
294 | array (
295 | 0 =>
296 | array (
297 | 'doc_write_failures' => 0,
298 | 'docs_read' => 2,
299 | 'docs_written' => 2,
300 | 'end_last_seq' => 5,
301 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
302 | 'missing_checked' => 2,
303 | 'missing_found' => 2,
304 | 'recorded_seq' => 5,
305 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
306 | 'start_last_seq' => 3,
307 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
308 | ),
309 | 1 =>
310 | array (
311 | 'doc_write_failures' => 0,
312 | 'docs_read' => 1,
313 | 'docs_written' => 1,
314 | 'end_last_seq' => 3,
315 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
316 | 'missing_checked' => 1,
317 | 'missing_found' => 1,
318 | 'recorded_seq' => 3,
319 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d',
320 | 'start_last_seq' => 2,
321 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
322 | ),
323 | 2 =>
324 | array (
325 | 'doc_write_failures' => 0,
326 | 'docs_read' => 2,
327 | 'docs_written' => 2,
328 | 'end_last_seq' => 2,
329 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
330 | 'missing_checked' => 2,
331 | 'missing_found' => 2,
332 | 'recorded_seq' => 2,
333 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155',
334 | 'start_last_seq' => 0,
335 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
336 | ),
337 | ),
338 | 'replication_id_version' => 3,
339 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
340 | 'source_last_seq' => 5,
341 | ),
342 | array (
343 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a',
344 | '_rev' => '0-8',
345 | 'history' =>
346 | array (
347 | 0 =>
348 | array (
349 | 'doc_write_failures' => 0,
350 | 'docs_read' => 2,
351 | 'docs_written' => 2,
352 | 'end_last_seq' => 5,
353 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
354 | 'missing_checked' => 2,
355 | 'missing_found' => 2,
356 | 'recorded_seq' => 5,
357 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
358 | 'start_last_seq' => 3,
359 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
360 | ),
361 | 1 =>
362 | array (
363 | 'doc_write_failures' => 0,
364 | 'docs_read' => 1,
365 | 'docs_written' => 1,
366 | 'end_last_seq' => 3,
367 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
368 | 'missing_checked' => 1,
369 | 'missing_found' => 1,
370 | 'recorded_seq' => 3,
371 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d',
372 | 'start_last_seq' => 2,
373 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
374 | ),
375 | 2 =>
376 | array (
377 | 'doc_write_failures' => 0,
378 | 'docs_read' => 2,
379 | 'docs_written' => 2,
380 | 'end_last_seq' => 2,
381 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
382 | 'missing_checked' => 2,
383 | 'missing_found' => 2,
384 | 'recorded_seq' => 2,
385 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155',
386 | 'start_last_seq' => 0,
387 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
388 | ),
389 | ),
390 | 'replication_id_version' => 3,
391 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
392 | 'source_last_seq' => 5,
393 | ),
394 | 5
395 | ),
396 | array(
397 | array (
398 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a',
399 | '_rev' => '0-8',
400 | 'history' =>
401 | array (
402 | 0 =>
403 | array (
404 | 'doc_write_failures' => 0,
405 | 'docs_read' => 2,
406 | 'docs_written' => 2,
407 | 'end_last_seq' => 5,
408 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
409 | 'missing_checked' => 2,
410 | 'missing_found' => 2,
411 | 'recorded_seq' => 5,
412 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
413 | 'start_last_seq' => 3,
414 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
415 | ),
416 | 1 =>
417 | array (
418 | 'doc_write_failures' => 0,
419 | 'docs_read' => 1,
420 | 'docs_written' => 1,
421 | 'end_last_seq' => 3,
422 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
423 | 'missing_checked' => 1,
424 | 'missing_found' => 1,
425 | 'recorded_seq' => 3,
426 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d',
427 | 'start_last_seq' => 2,
428 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
429 | ),
430 | 2 =>
431 | array (
432 | 'doc_write_failures' => 0,
433 | 'docs_read' => 2,
434 | 'docs_written' => 2,
435 | 'end_last_seq' => 2,
436 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
437 | 'missing_checked' => 2,
438 | 'missing_found' => 2,
439 | 'recorded_seq' => 2,
440 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155',
441 | 'start_last_seq' => 0,
442 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
443 | ),
444 | ),
445 | 'replication_id_version' => 3,
446 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
447 | 'source_last_seq' => 5,
448 | ),
449 | array (
450 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a',
451 | '_rev' => '0-8',
452 | 'history' =>
453 | array (
454 | 0 =>
455 | array (
456 | 'doc_write_failures' => 0,
457 | 'docs_read' => 2,
458 | 'docs_written' => 2,
459 | 'end_last_seq' => 5,
460 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
461 | 'missing_checked' => 2,
462 | 'missing_found' => 2,
463 | 'recorded_seq' => 5,
464 | 'session_id' => 'cbbdafa70e0db5cb57d02a6b955',
465 | 'start_last_seq' => 3,
466 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
467 | ),
468 | 1 =>
469 | array (
470 | 'doc_write_failures' => 0,
471 | 'docs_read' => 1,
472 | 'docs_written' => 1,
473 | 'end_last_seq' => 3,
474 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
475 | 'missing_checked' => 1,
476 | 'missing_found' => 1,
477 | 'recorded_seq' => 3,
478 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d',
479 | 'start_last_seq' => 2,
480 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
481 | ),
482 | 2 =>
483 | array (
484 | 'doc_write_failures' => 0,
485 | 'docs_read' => 2,
486 | 'docs_written' => 2,
487 | 'end_last_seq' => 2,
488 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
489 | 'missing_checked' => 2,
490 | 'missing_found' => 2,
491 | 'recorded_seq' => 2,
492 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155',
493 | 'start_last_seq' => 0,
494 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
495 | ),
496 | ),
497 | 'replication_id_version' => 3,
498 | 'session_id' => 'zzz34cbbdafa70e0db5cb57d02a6b955',
499 | 'source_last_seq' => 5,
500 | ),
501 | 3,
502 |
503 | ),
504 | array(
505 | array (
506 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a',
507 | '_rev' => '0-8',
508 | 'history' =>
509 | array (
510 | 0 =>
511 | array (
512 | 'doc_write_failures' => 0,
513 | 'docs_read' => 2,
514 | 'docs_written' => 2,
515 | 'end_last_seq' => 5,
516 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
517 | 'missing_checked' => 2,
518 | 'missing_found' => 2,
519 | 'recorded_seq' => 5,
520 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
521 | 'start_last_seq' => 3,
522 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT',
523 | ),
524 | 1 =>
525 | array (
526 | 'doc_write_failures' => 0,
527 | 'docs_read' => 1,
528 | 'docs_written' => 1,
529 | 'end_last_seq' => 3,
530 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
531 | 'missing_checked' => 1,
532 | 'missing_found' => 1,
533 | 'recorded_seq' => 3,
534 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d',
535 | 'start_last_seq' => 2,
536 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT',
537 | ),
538 | 2 =>
539 | array (
540 | 'doc_write_failures' => 0,
541 | 'docs_read' => 2,
542 | 'docs_written' => 2,
543 | 'end_last_seq' => 2,
544 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
545 | 'missing_checked' => 2,
546 | 'missing_found' => 2,
547 | 'recorded_seq' => 2,
548 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155',
549 | 'start_last_seq' => 0,
550 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT',
551 | ),
552 | ),
553 | 'replication_id_version' => 3,
554 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955',
555 | 'source_last_seq' => 5,
556 | ),
557 | null,
558 | 0
559 | ),
560 | array(
561 | null,
562 | null,
563 | 0
564 | )
565 | );
566 | }
567 |
568 | /**
569 | * Test the mapping done in getMapping
570 | *
571 | * @dataProvider changesFeedProvider
572 | */
573 | public function testGetMapping($changes, $continuous, $expected)
574 | {
575 | $task = new ReplicationTask();
576 | $task->setContinuous($continuous);
577 | $replication = new Replication($this->source, $this->target, $task);
578 | $mapping = $replication->getMapping($changes);
579 | $this->assertEquals($expected, $mapping, 'Incorrect mapping in getMapping.');
580 |
581 | }
582 |
583 | public function changesFeedProvider()
584 | {
585 | $normal = array (
586 | 'results' =>
587 | array (
588 | 0 =>
589 | array (
590 | 'seq' => 14,
591 | 'id' => 'f957f41e',
592 | 'changes' =>
593 | array (
594 | 0 =>
595 | array (
596 | 'rev' => '3-46a3',
597 | ),
598 | ),
599 | 'deleted' => true,
600 | ),
601 | 1 =>
602 | array (
603 | 'seq' => 29,
604 | 'id' => 'ddf339dd',
605 | 'changes' =>
606 | array (
607 | 0 =>
608 | array (
609 | 'rev' => '10-304b',
610 | ),
611 | ),
612 | ),
613 | 2 =>
614 | array (
615 | 'seq' => 39,
616 | 'id' => 'f13bd08b',
617 | 'changes' =>
618 | array (
619 | 0 =>
620 | array (
621 | 'rev' => '1-b35d',
622 | ),
623 | array(
624 | 'rev' => '1-535d',
625 | )
626 | ),
627 | ),
628 | ),
629 | 'last_seq' => 78,
630 | );
631 | $continuous = '{"seq":14,"id":"f957f41e","changes":[{"rev":"3-46a3"}],"deleted":true}
632 | {"seq":29,"id":"ddf339dd","changes":[{"rev":"10-304b"}]}
633 | {"seq":39,"id":"f13bd08b","changes":[{"rev":"1-b35d"},{"rev":"1-535d"}]}';
634 |
635 | $expected = array (
636 | 'f957f41e' =>
637 | array (
638 | 0 => '3-46a3',
639 | ),
640 | 'ddf339dd' =>
641 | array (
642 | 0 => '10-304b',
643 | ),
644 | 'f13bd08b' =>
645 | array (
646 | 0 => '1-b35d',
647 | 1 => '1-535d',
648 | ),
649 | );
650 | return array(
651 | array(
652 | $normal,
653 | false,
654 | $expected
655 | ),
656 | array(
657 | $continuous,
658 | true,
659 | $expected
660 | )
661 | );
662 | }
663 |
664 | protected function tearDown()
665 | {
666 | }
667 | }
668 |
--------------------------------------------------------------------------------
/tests/ReplicatorFunctionalTestBase.php:
--------------------------------------------------------------------------------
1 | getSourceTestDatabase()
26 | );
27 | }
28 |
29 | public function getTargetCouchDBClient()
30 | {
31 | return new CouchDBClient(
32 | new SocketClient(),
33 | $this->getTargetTestDatabase()
34 | );
35 | }
36 |
37 | public function getReplicationTask()
38 | {
39 | return new ReplicationTask();
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/tests/TestUtil.php:
--------------------------------------------------------------------------------
1 |