Commit | Line | Data |
---|---|---|
07c09ae4 EM |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
81621fee | 4 | | CiviCRM version 4.7 | |
07c09ae4 | 5 | +--------------------------------------------------------------------+ |
15a4309a | 6 | | Copyright CiviCRM LLC (c) 2004-2017 | |
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 | * | |
15a4309a | 34 | * @copyright CiviCRM LLC (c) 2004-2017 |
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 |
b4a332a9 | 42 | * @group civimail |
07c09ae4 EM |
43 | */ |
44 | class api_v3_JobProcessMailingTest extends CiviUnitTestCase { | |
45 | protected $_apiversion = 3; | |
46 | ||
47 | public $DBResetRequired = FALSE; | |
48 | public $_entity = 'Job'; | |
49 | public $_params = array(); | |
50 | private $_groupID; | |
51 | private $_email; | |
52 | ||
d667a9ba TO |
53 | protected $defaultSettings; |
54 | ||
07c09ae4 EM |
55 | /** |
56 | * @var CiviMailUtils | |
57 | */ | |
58 | private $_mut; | |
59 | ||
00be9182 | 60 | public function setUp() { |
d667a9ba | 61 | $this->cleanupMailingTest(); |
07c09ae4 | 62 | parent::setUp(); |
97b7d4a0 | 63 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW |
07c09ae4 EM |
64 | $this->_groupID = $this->groupCreate(); |
65 | $this->_email = 'test@test.test'; | |
66 | $this->_params = array( | |
67 | 'subject' => 'Accidents in cars cause children', | |
21b09c13 | 68 | 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.', |
07c09ae4 EM |
69 | 'name' => 'mailing name', |
70 | 'created_id' => 1, | |
71 | 'groups' => array('include' => array($this->_groupID)), | |
5f445749 | 72 | 'scheduled_date' => 'now', |
07c09ae4 | 73 | ); |
d667a9ba | 74 | $this->defaultSettings = array( |
502eefb0 | 75 | 'mailings' => 1, // int, #mailings to send |
d667a9ba TO |
76 | 'recipients' => 20, // int, #contacts to receive mailing |
77 | 'workers' => 1, // int, #concurrent cron jobs | |
78 | 'iterations' => 1, // int, #times to spawn all the workers | |
79 | 'lockHold' => 0, // int, #extra seconds each cron job should hold lock | |
80 | 'mailerBatchLimit' => 0, // int, max# recipients to send in a given cron run | |
81 | 'mailerJobsMax' => 0, // int, max# concurrent jobs | |
82 | 'mailerJobSize' => 0, // int, max# recipients in each job | |
83 | 'mailThrottleTime' => 0, // int, microseconds separating messages | |
84 | ); | |
481a74f4 | 85 | $this->_mut = new CiviMailUtils($this, TRUE); |
07c09ae4 EM |
86 | $this->callAPISuccess('mail_settings', 'get', array('api.mail_settings.create' => array('domain' => 'chaos.org'))); |
87 | } | |
88 | ||
89 | /** | |
07c09ae4 | 90 | */ |
00be9182 | 91 | public function tearDown() { |
d667a9ba | 92 | //$this->_mut->clearMessages(); |
07c09ae4 | 93 | $this->_mut->stop(); |
07c09ae4 | 94 | CRM_Utils_Hook::singleton()->reset(); |
97b7d4a0 | 95 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW |
d667a9ba | 96 | //$this->cleanupMailingTest(); |
07c09ae4 | 97 | parent::tearDown(); |
07c09ae4 EM |
98 | } |
99 | ||
d667a9ba | 100 | public function testBasic() { |
07c09ae4 | 101 | $this->createContactsInGroup(10, $this->_groupID); |
dc00ac6d | 102 | Civi::settings()->add(array( |
d667a9ba TO |
103 | 'mailerBatchLimit' => 2, |
104 | )); | |
07c09ae4 | 105 | $this->callAPISuccess('mailing', 'create', $this->_params); |
d667a9ba | 106 | $this->_mut->assertRecipients(array()); |
07c09ae4 EM |
107 | $this->callAPISuccess('job', 'process_mailing', array()); |
108 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
109 | } | |
110 | ||
f008885c E |
111 | /** |
112 | * Test mail when in non-production environment. | |
113 | * | |
114 | */ | |
115 | public function testMailNonProductionRun() { | |
116 | // Test in non-production mode. | |
117 | $params = array( | |
118 | 'environment' => 'Staging', | |
119 | ); | |
120 | $this->callAPISuccess('Setting', 'create', $params); | |
288e5f75 JP |
121 | //Assert if outbound mail is disabled. |
122 | $mailingBackend = Civi::settings()->get('mailing_backend'); | |
123 | $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED); | |
124 | ||
f008885c E |
125 | $this->createContactsInGroup(10, $this->_groupID); |
126 | Civi::settings()->add(array( | |
127 | 'mailerBatchLimit' => 2, | |
128 | )); | |
129 | $this->callAPISuccess('mailing', 'create', $this->_params); | |
130 | $this->_mut->assertRecipients(array()); | |
131 | $this->callAPIFailure('job', 'process_mailing', "Failure in api call for job process_mailing: Job has not been executed as it is a non-production environment."); | |
132 | ||
133 | // Test with runInNonProductionEnvironment param. | |
134 | $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE)); | |
135 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
136 | ||
288e5f75 JP |
137 | $jobId = $this->callAPISuccessGetValue('Job', array( |
138 | 'return' => "id", | |
139 | 'api_action' => "group_rebuild", | |
140 | )); | |
141 | $this->callAPISuccess('Job', 'create', array( | |
142 | 'id' => $jobId, | |
143 | 'parameters' => "runInNonProductionEnvironment=TRUE", | |
144 | )); | |
145 | $jobManager = new CRM_Core_JobManager(); | |
146 | $jobManager->executeJobById($jobId); | |
147 | ||
148 | //Assert if outbound mail is still disabled. | |
149 | $mailingBackend = Civi::settings()->get('mailing_backend'); | |
150 | $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED); | |
151 | ||
f008885c E |
152 | // Test in production mode. |
153 | $params = array( | |
154 | 'environment' => 'Production', | |
155 | ); | |
156 | $this->callAPISuccess('Setting', 'create', $params); | |
157 | $this->callAPISuccess('job', 'process_mailing', array()); | |
158 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
159 | } | |
160 | ||
d667a9ba TO |
161 | public function concurrencyExamples() { |
162 | $es = array(); | |
163 | ||
164 | // Launch 3 workers, but mailerJobsMax limits us to 1 worker. | |
165 | $es[0] = array( | |
166 | array( | |
167 | 'recipients' => 20, | |
168 | 'workers' => 3, | |
a2341a99 TO |
169 | // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because |
170 | // the data.* locks trample the worker.* locks. However, setting lockHold allows us to | |
171 | // approximate the behavior of what would happen *if* the lock-implementation didn't suffer | |
172 | // trampling effects. | |
d667a9ba TO |
173 | 'lockHold' => 10, |
174 | 'mailerBatchLimit' => 4, | |
175 | 'mailerJobsMax' => 1, | |
176 | ), | |
177 | array( | |
178 | 0 => 2, // 2 jobs which produce 0 messages | |
179 | 4 => 1, // 1 job which produces 4 messages | |
180 | ), | |
181 | 4, | |
182 | ); | |
183 | ||
184 | // Launch 3 workers, but mailerJobsMax limits us to 2 workers. | |
185 | $es[1] = array( | |
186 | array(// Settings. | |
187 | 'recipients' => 20, | |
188 | 'workers' => 3, | |
a2341a99 TO |
189 | // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because |
190 | // the data.* locks trample the worker.* locks. However, setting lockHold allows us to | |
191 | // approximate the behavior of what would happen *if* the lock-implementation didn't suffer | |
192 | // trampling effects. | |
d667a9ba TO |
193 | 'lockHold' => 10, |
194 | 'mailerBatchLimit' => 5, | |
195 | 'mailerJobsMax' => 2, | |
196 | ), | |
197 | array(// Tallies. | |
198 | 0 => 1, // 1 job which produce 0 messages | |
199 | 5 => 2, // 2 jobs which produce 5 messages | |
200 | ), | |
201 | 10, // Total sent. | |
202 | ); | |
203 | ||
204 | // Launch 3 workers and saturate them (mailerJobsMax=3) | |
205 | $es[2] = array( | |
206 | array(// Settings. | |
207 | 'recipients' => 20, | |
208 | 'workers' => 3, | |
d667a9ba TO |
209 | 'mailerBatchLimit' => 6, |
210 | 'mailerJobsMax' => 3, | |
211 | ), | |
212 | array(// Tallies. | |
213 | 6 => 3, // 3 jobs which produce 6 messages | |
214 | ), | |
215 | 18, // Total sent. | |
216 | ); | |
217 | ||
218 | // Launch 4 workers and saturate them (mailerJobsMax=0) | |
219 | $es[3] = array( | |
220 | array(// Settings. | |
221 | 'recipients' => 20, | |
222 | 'workers' => 4, | |
d667a9ba TO |
223 | 'mailerBatchLimit' => 6, |
224 | 'mailerJobsMax' => 0, | |
225 | ), | |
226 | array(// Tallies. | |
227 | 6 => 3, // 3 jobs which produce 6 messages | |
228 | 2 => 1, // 1 job which produces 2 messages | |
229 | ), | |
230 | 20, // Total sent. | |
231 | ); | |
232 | ||
233 | // Launch 1 worker, 3 times in a row. Deliver everything. | |
234 | $es[4] = array( | |
235 | array(// Settings. | |
236 | 'recipients' => 10, | |
237 | 'workers' => 1, | |
238 | 'iterations' => 3, | |
239 | 'mailerBatchLimit' => 7, | |
240 | ), | |
241 | array(// Tallies. | |
242 | 7 => 1, // 1 job which produces 7 messages | |
243 | 3 => 1, // 1 job which produces 3 messages | |
244 | 0 => 1, // 1 job which produces 0 messages | |
245 | ), | |
246 | 10, // Total sent. | |
247 | ); | |
248 | ||
249 | // Launch 2 worker, 3 times in a row. Deliver everything. | |
250 | $es[5] = array( | |
251 | array(// Settings. | |
252 | 'recipients' => 10, | |
253 | 'workers' => 2, | |
254 | 'iterations' => 3, | |
255 | 'mailerBatchLimit' => 3, | |
256 | ), | |
257 | array(// Tallies. | |
258 | 3 => 3, // 3 jobs which produce 3 messages | |
259 | 1 => 1, // 1 job which produces 1 messages | |
260 | 0 => 2, // 2 jobs which produce 0 messages | |
261 | ), | |
262 | 10, // Total sent. | |
263 | ); | |
264 | ||
502eefb0 TO |
265 | // For two mailings, launch 1 worker, 5 times in a row. Deliver everything. |
266 | $es[6] = array( | |
267 | array(// Settings. | |
268 | 'mailings' => 2, | |
269 | 'recipients' => 10, | |
270 | 'workers' => 1, | |
271 | 'iterations' => 5, | |
272 | 'mailerBatchLimit' => 6, | |
273 | ), | |
274 | array(// Tallies. | |
275 | // x6 => x4+x2 => x6 => x2 => x0 | |
276 | 6 => 3, // 3 jobs which produce 6 messages | |
277 | 2 => 1, // 1 job which produces 2 messages | |
278 | 0 => 1, // 1 job which produces 0 messages | |
279 | ), | |
280 | 20, // Total sent. | |
281 | ); | |
282 | ||
d667a9ba TO |
283 | return $es; |
284 | } | |
285 | ||
286 | /** | |
287 | * Setup various mail configuration options (eg $mailerBatchLimit, | |
288 | * $mailerJobMax) and spawn multiple worker threads ($workers). | |
289 | * Allow the threads to complete. (Optionally, repeat the above | |
290 | * process.) Finally, check to see if the right number of | |
291 | * jobs delivered the right number of messages. | |
292 | * | |
293 | * @param array $settings | |
294 | * An array of settings (eg mailerBatchLimit, workers). See comments | |
295 | * for $this->defaultSettings. | |
296 | * @param array $expectedTallies | |
297 | * A listing of the number cron-runs keyed by their size. | |
298 | * For example, array(10=>2) means that there 2 cron-runs | |
299 | * which delivered 10 messages each. | |
300 | * @param int $expectedTotal | |
301 | * The total number of contacts for whom messages should have | |
302 | * been sent. | |
303 | * @dataProvider concurrencyExamples | |
304 | */ | |
305 | public function testConcurrency($settings, $expectedTallies, $expectedTotal) { | |
306 | $settings = array_merge($this->defaultSettings, $settings); | |
307 | ||
308 | $this->createContactsInGroup($settings['recipients'], $this->_groupID); | |
dc00ac6d | 309 | Civi::settings()->add(CRM_Utils_Array::subset($settings, array( |
d667a9ba TO |
310 | 'mailerBatchLimit', |
311 | 'mailerJobsMax', | |
312 | 'mailThrottleTime', | |
313 | ))); | |
314 | ||
502eefb0 TO |
315 | for ($i = 0; $i < $settings['mailings']; $i++) { |
316 | $this->callAPISuccess('mailing', 'create', $this->_params); | |
317 | } | |
d667a9ba TO |
318 | |
319 | $this->_mut->assertRecipients(array()); | |
320 | ||
321 | $allApiResults = array(); | |
322 | for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++) { | |
323 | $apiCalls = $this->createExternalAPI(); | |
324 | $apiCalls->addEnv(array('CIVICRM_CRON_HOLD' => $settings['lockHold'])); | |
325 | for ($workerId = 0; $workerId < $settings['workers']; $workerId++) { | |
326 | $apiCalls->addCall('job', 'process_mailing', array()); | |
327 | } | |
328 | $apiCalls->start(); | |
329 | $this->assertEquals($settings['workers'], $apiCalls->getRunningCount()); | |
330 | ||
331 | $apiCalls->wait(); | |
332 | $allApiResults = array_merge($allApiResults, $apiCalls->getResults()); | |
333 | } | |
334 | ||
335 | $actualTallies = $this->tallyApiResults($allApiResults); | |
336 | $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r(array( | |
337 | 'expectedTallies' => $expectedTallies, | |
338 | 'actualTallies' => $actualTallies, | |
339 | 'apiResults' => $allApiResults, | |
340 | ), TRUE)); | |
502eefb0 | 341 | $this->_mut->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings'])); |
d667a9ba TO |
342 | $this->assertEquals(0, $apiCalls->getRunningCount()); |
343 | } | |
344 | ||
07c09ae4 | 345 | /** |
54957108 | 346 | * Create contacts in group. |
347 | * | |
e16033b4 TO |
348 | * @param int $count |
349 | * @param int $groupID | |
54957108 | 350 | * @param string $domain |
07c09ae4 | 351 | */ |
d667a9ba | 352 | public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') { |
481a74f4 | 353 | for ($i = 1; $i <= $count; $i++) { |
d667a9ba | 354 | $contactID = $this->individualCreate(array('first_name' => $count, 'email' => 'mail' . $i . '@' . $domain)); |
92915c55 | 355 | $this->callAPISuccess('group_contact', 'create', array( |
d667a9ba TO |
356 | 'contact_id' => $contactID, |
357 | 'group_id' => $groupID, | |
358 | 'status' => 'Added', | |
359 | )); | |
07c09ae4 EM |
360 | } |
361 | } | |
362 | ||
363 | /** | |
d667a9ba TO |
364 | * Construct the list of email addresses for $count recipients. |
365 | * | |
e16033b4 TO |
366 | * @param int $start |
367 | * @param int $count | |
502eefb0 TO |
368 | * @param string $domain |
369 | * @param int $mailings | |
07c09ae4 EM |
370 | * |
371 | * @return array | |
372 | */ | |
502eefb0 | 373 | public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) { |
07c09ae4 | 374 | $recipients = array(); |
502eefb0 TO |
375 | for ($m = 0; $m < $mailings; $m++) { |
376 | for ($i = $start; $i < ($start + $count); $i++) { | |
377 | $recipients[][0] = 'mail' . $i . '@' . $domain; | |
378 | } | |
07c09ae4 EM |
379 | } |
380 | return $recipients; | |
381 | } | |
96025800 | 382 | |
d667a9ba TO |
383 | protected function cleanupMailingTest() { |
384 | $this->quickCleanup(array( | |
385 | 'civicrm_mailing', | |
386 | 'civicrm_mailing_job', | |
387 | 'civicrm_mailing_spool', | |
388 | 'civicrm_mailing_group', | |
389 | 'civicrm_mailing_recipients', | |
390 | 'civicrm_mailing_event_queue', | |
391 | 'civicrm_mailing_event_bounce', | |
392 | 'civicrm_mailing_event_delivered', | |
393 | 'civicrm_group', | |
394 | 'civicrm_group_contact', | |
395 | 'civicrm_contact', | |
396 | )); | |
397 | } | |
398 | ||
399 | /** | |
400 | * Categorize results based on (a) whether they succeeded | |
401 | * and (b) the number of messages sent. | |
402 | * | |
403 | * @param array $apiResults | |
404 | * @return array | |
405 | * One key 'error' for all failures. | |
406 | * A separate key for each distinct quantity. | |
407 | */ | |
408 | protected function tallyApiResults($apiResults) { | |
409 | $ret = array(); | |
410 | foreach ($apiResults as $apiResult) { | |
411 | $key = !empty($apiResult['is_error']) ? 'error' : $apiResult['values']['processed']; | |
412 | $ret[$key] = !empty($ret[$key]) ? 1 + $ret[$key] : 1; | |
413 | } | |
414 | return $ret; | |
415 | } | |
416 | ||
07c09ae4 | 417 | } |