Merge pull request #7797 from JKingsnorth/CRM-17977
[civicrm-core.git] / tests / phpunit / api / v3 / JobProcessMailingTest.php
CommitLineData
07c09ae4
EM
1<?php
2/*
3 +--------------------------------------------------------------------+
81621fee 4 | CiviCRM version 4.7 |
07c09ae4 5 +--------------------------------------------------------------------+
fa938177 6 | Copyright CiviCRM LLC (c) 2004-2016 |
07c09ae4
EM
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28/**
29 * File for the CiviCRM APIv3 job functions
30 *
31 * @package CiviCRM_APIv3
32 * @subpackage API_Job
33 *
fa938177 34 * @copyright CiviCRM LLC (c) 2004-2016
07c09ae4
EM
35 * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
36 *
37 */
0eea664b 38
07c09ae4
EM
39/**
40 * Class api_v3_JobTest
acb109b7 41 * @group headless
07c09ae4
EM
42 */
43class api_v3_JobProcessMailingTest extends CiviUnitTestCase {
44 protected $_apiversion = 3;
45
46 public $DBResetRequired = FALSE;
47 public $_entity = 'Job';
48 public $_params = array();
49 private $_groupID;
50 private $_email;
51
d667a9ba
TO
52 protected $defaultSettings;
53
07c09ae4
EM
54 /**
55 * @var CiviMailUtils
56 */
57 private $_mut;
58
00be9182 59 public function setUp() {
d667a9ba 60 $this->cleanupMailingTest();
07c09ae4 61 parent::setUp();
97b7d4a0 62 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW
07c09ae4
EM
63 $this->_groupID = $this->groupCreate();
64 $this->_email = 'test@test.test';
65 $this->_params = array(
66 'subject' => 'Accidents in cars cause children',
21b09c13 67 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.',
07c09ae4
EM
68 'name' => 'mailing name',
69 'created_id' => 1,
70 'groups' => array('include' => array($this->_groupID)),
5f445749 71 'scheduled_date' => 'now',
07c09ae4 72 );
d667a9ba 73 $this->defaultSettings = array(
502eefb0 74 'mailings' => 1, // int, #mailings to send
d667a9ba
TO
75 'recipients' => 20, // int, #contacts to receive mailing
76 'workers' => 1, // int, #concurrent cron jobs
77 'iterations' => 1, // int, #times to spawn all the workers
78 'lockHold' => 0, // int, #extra seconds each cron job should hold lock
79 'mailerBatchLimit' => 0, // int, max# recipients to send in a given cron run
80 'mailerJobsMax' => 0, // int, max# concurrent jobs
81 'mailerJobSize' => 0, // int, max# recipients in each job
82 'mailThrottleTime' => 0, // int, microseconds separating messages
83 );
481a74f4 84 $this->_mut = new CiviMailUtils($this, TRUE);
07c09ae4
EM
85 $this->callAPISuccess('mail_settings', 'get', array('api.mail_settings.create' => array('domain' => 'chaos.org')));
86 }
87
88 /**
07c09ae4 89 */
00be9182 90 public function tearDown() {
d667a9ba 91 //$this->_mut->clearMessages();
07c09ae4 92 $this->_mut->stop();
07c09ae4 93 CRM_Utils_Hook::singleton()->reset();
97b7d4a0 94 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW
d667a9ba 95 //$this->cleanupMailingTest();
07c09ae4 96 parent::tearDown();
07c09ae4
EM
97 }
98
d667a9ba 99 public function testBasic() {
07c09ae4 100 $this->createContactsInGroup(10, $this->_groupID);
dc00ac6d 101 Civi::settings()->add(array(
d667a9ba
TO
102 'mailerBatchLimit' => 2,
103 ));
07c09ae4 104 $this->callAPISuccess('mailing', 'create', $this->_params);
d667a9ba 105 $this->_mut->assertRecipients(array());
07c09ae4
EM
106 $this->callAPISuccess('job', 'process_mailing', array());
107 $this->_mut->assertRecipients($this->getRecipients(1, 2));
108 }
109
d667a9ba
TO
110 public function concurrencyExamples() {
111 $es = array();
112
113 // Launch 3 workers, but mailerJobsMax limits us to 1 worker.
114 $es[0] = array(
115 array(
116 'recipients' => 20,
117 'workers' => 3,
a2341a99
TO
118 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
119 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
120 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
121 // trampling effects.
d667a9ba
TO
122 'lockHold' => 10,
123 'mailerBatchLimit' => 4,
124 'mailerJobsMax' => 1,
125 ),
126 array(
127 0 => 2, // 2 jobs which produce 0 messages
128 4 => 1, // 1 job which produces 4 messages
129 ),
130 4,
131 );
132
133 // Launch 3 workers, but mailerJobsMax limits us to 2 workers.
134 $es[1] = array(
135 array(// Settings.
136 'recipients' => 20,
137 'workers' => 3,
a2341a99
TO
138 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
139 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
140 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
141 // trampling effects.
d667a9ba
TO
142 'lockHold' => 10,
143 'mailerBatchLimit' => 5,
144 'mailerJobsMax' => 2,
145 ),
146 array(// Tallies.
147 0 => 1, // 1 job which produce 0 messages
148 5 => 2, // 2 jobs which produce 5 messages
149 ),
150 10, // Total sent.
151 );
152
153 // Launch 3 workers and saturate them (mailerJobsMax=3)
154 $es[2] = array(
155 array(// Settings.
156 'recipients' => 20,
157 'workers' => 3,
d667a9ba
TO
158 'mailerBatchLimit' => 6,
159 'mailerJobsMax' => 3,
160 ),
161 array(// Tallies.
162 6 => 3, // 3 jobs which produce 6 messages
163 ),
164 18, // Total sent.
165 );
166
167 // Launch 4 workers and saturate them (mailerJobsMax=0)
168 $es[3] = array(
169 array(// Settings.
170 'recipients' => 20,
171 'workers' => 4,
d667a9ba
TO
172 'mailerBatchLimit' => 6,
173 'mailerJobsMax' => 0,
174 ),
175 array(// Tallies.
176 6 => 3, // 3 jobs which produce 6 messages
177 2 => 1, // 1 job which produces 2 messages
178 ),
179 20, // Total sent.
180 );
181
182 // Launch 1 worker, 3 times in a row. Deliver everything.
183 $es[4] = array(
184 array(// Settings.
185 'recipients' => 10,
186 'workers' => 1,
187 'iterations' => 3,
188 'mailerBatchLimit' => 7,
189 ),
190 array(// Tallies.
191 7 => 1, // 1 job which produces 7 messages
192 3 => 1, // 1 job which produces 3 messages
193 0 => 1, // 1 job which produces 0 messages
194 ),
195 10, // Total sent.
196 );
197
198 // Launch 2 worker, 3 times in a row. Deliver everything.
199 $es[5] = array(
200 array(// Settings.
201 'recipients' => 10,
202 'workers' => 2,
203 'iterations' => 3,
204 'mailerBatchLimit' => 3,
205 ),
206 array(// Tallies.
207 3 => 3, // 3 jobs which produce 3 messages
208 1 => 1, // 1 job which produces 1 messages
209 0 => 2, // 2 jobs which produce 0 messages
210 ),
211 10, // Total sent.
212 );
213
502eefb0
TO
214 // For two mailings, launch 1 worker, 5 times in a row. Deliver everything.
215 $es[6] = array(
216 array(// Settings.
217 'mailings' => 2,
218 'recipients' => 10,
219 'workers' => 1,
220 'iterations' => 5,
221 'mailerBatchLimit' => 6,
222 ),
223 array(// Tallies.
224 // x6 => x4+x2 => x6 => x2 => x0
225 6 => 3, // 3 jobs which produce 6 messages
226 2 => 1, // 1 job which produces 2 messages
227 0 => 1, // 1 job which produces 0 messages
228 ),
229 20, // Total sent.
230 );
231
d667a9ba
TO
232 return $es;
233 }
234
235 /**
236 * Setup various mail configuration options (eg $mailerBatchLimit,
237 * $mailerJobMax) and spawn multiple worker threads ($workers).
238 * Allow the threads to complete. (Optionally, repeat the above
239 * process.) Finally, check to see if the right number of
240 * jobs delivered the right number of messages.
241 *
242 * @param array $settings
243 * An array of settings (eg mailerBatchLimit, workers). See comments
244 * for $this->defaultSettings.
245 * @param array $expectedTallies
246 * A listing of the number cron-runs keyed by their size.
247 * For example, array(10=>2) means that there 2 cron-runs
248 * which delivered 10 messages each.
249 * @param int $expectedTotal
250 * The total number of contacts for whom messages should have
251 * been sent.
252 * @dataProvider concurrencyExamples
253 */
254 public function testConcurrency($settings, $expectedTallies, $expectedTotal) {
255 $settings = array_merge($this->defaultSettings, $settings);
256
257 $this->createContactsInGroup($settings['recipients'], $this->_groupID);
dc00ac6d 258 Civi::settings()->add(CRM_Utils_Array::subset($settings, array(
d667a9ba
TO
259 'mailerBatchLimit',
260 'mailerJobsMax',
261 'mailThrottleTime',
262 )));
263
502eefb0
TO
264 for ($i = 0; $i < $settings['mailings']; $i++) {
265 $this->callAPISuccess('mailing', 'create', $this->_params);
266 }
d667a9ba
TO
267
268 $this->_mut->assertRecipients(array());
269
270 $allApiResults = array();
271 for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++) {
272 $apiCalls = $this->createExternalAPI();
273 $apiCalls->addEnv(array('CIVICRM_CRON_HOLD' => $settings['lockHold']));
274 for ($workerId = 0; $workerId < $settings['workers']; $workerId++) {
275 $apiCalls->addCall('job', 'process_mailing', array());
276 }
277 $apiCalls->start();
278 $this->assertEquals($settings['workers'], $apiCalls->getRunningCount());
279
280 $apiCalls->wait();
281 $allApiResults = array_merge($allApiResults, $apiCalls->getResults());
282 }
283
284 $actualTallies = $this->tallyApiResults($allApiResults);
285 $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r(array(
286 'expectedTallies' => $expectedTallies,
287 'actualTallies' => $actualTallies,
288 'apiResults' => $allApiResults,
289 ), TRUE));
502eefb0 290 $this->_mut->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings']));
d667a9ba
TO
291 $this->assertEquals(0, $apiCalls->getRunningCount());
292 }
293
07c09ae4 294 /**
54957108 295 * Create contacts in group.
296 *
e16033b4
TO
297 * @param int $count
298 * @param int $groupID
54957108 299 * @param string $domain
07c09ae4 300 */
d667a9ba 301 public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') {
481a74f4 302 for ($i = 1; $i <= $count; $i++) {
d667a9ba 303 $contactID = $this->individualCreate(array('first_name' => $count, 'email' => 'mail' . $i . '@' . $domain));
92915c55 304 $this->callAPISuccess('group_contact', 'create', array(
d667a9ba
TO
305 'contact_id' => $contactID,
306 'group_id' => $groupID,
307 'status' => 'Added',
308 ));
07c09ae4
EM
309 }
310 }
311
312 /**
d667a9ba
TO
313 * Construct the list of email addresses for $count recipients.
314 *
e16033b4
TO
315 * @param int $start
316 * @param int $count
502eefb0
TO
317 * @param string $domain
318 * @param int $mailings
07c09ae4
EM
319 *
320 * @return array
321 */
502eefb0 322 public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) {
07c09ae4 323 $recipients = array();
502eefb0
TO
324 for ($m = 0; $m < $mailings; $m++) {
325 for ($i = $start; $i < ($start + $count); $i++) {
326 $recipients[][0] = 'mail' . $i . '@' . $domain;
327 }
07c09ae4
EM
328 }
329 return $recipients;
330 }
96025800 331
d667a9ba
TO
332 protected function cleanupMailingTest() {
333 $this->quickCleanup(array(
334 'civicrm_mailing',
335 'civicrm_mailing_job',
336 'civicrm_mailing_spool',
337 'civicrm_mailing_group',
338 'civicrm_mailing_recipients',
339 'civicrm_mailing_event_queue',
340 'civicrm_mailing_event_bounce',
341 'civicrm_mailing_event_delivered',
342 'civicrm_group',
343 'civicrm_group_contact',
344 'civicrm_contact',
345 ));
346 }
347
348 /**
349 * Categorize results based on (a) whether they succeeded
350 * and (b) the number of messages sent.
351 *
352 * @param array $apiResults
353 * @return array
354 * One key 'error' for all failures.
355 * A separate key for each distinct quantity.
356 */
357 protected function tallyApiResults($apiResults) {
358 $ret = array();
359 foreach ($apiResults as $apiResult) {
360 $key = !empty($apiResult['is_error']) ? 'error' : $apiResult['values']['processed'];
361 $ret[$key] = !empty($ret[$key]) ? 1 + $ret[$key] : 1;
362 }
363 return $ret;
364 }
365
07c09ae4 366}