3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
29 * File for the CiviCRM APIv3 job functions
31 * @package CiviCRM_APIv3
34 * @copyright CiviCRM LLC (c) 2004-2019
35 * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
40 * Class api_v3_JobTest
44 class api_v3_JobProcessMailingTest
extends CiviUnitTestCase
{
45 protected $_apiversion = 3;
47 public $DBResetRequired = FALSE;
48 public $_entity = 'Job';
53 protected $defaultSettings;
60 public function setUp() {
61 $this->cleanupMailingTest();
64 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
65 $this->_groupID
= $this->groupCreate();
66 $this->_email
= 'test@test.test';
68 'subject' => 'Accidents in cars cause children',
69 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.',
70 'name' => 'mailing name',
72 'groups' => ['include' => [$this->_groupID
]],
73 'scheduled_date' => 'now',
75 $this->defaultSettings
= [
76 // int, #mailings to send
78 // int, #contacts to receive mailing
80 // int, #concurrent cron jobs
82 // int, #times to spawn all the workers
84 // int, #extra seconds each cron job should hold lock
86 // int, max# recipients to send in a given cron run
87 'mailerBatchLimit' => 0,
88 // int, max# concurrent jobs
90 // int, max# recipients in each job
92 // int, microseconds separating messages
93 'mailThrottleTime' => 0,
95 $this->_mut
= new CiviMailUtils($this, TRUE);
96 $this->callAPISuccess('mail_settings', 'get', ['api.mail_settings.create' => ['domain' => 'chaos.org']]);
101 public function tearDown() {
102 //$this->_mut->clearMessages();
104 CRM_Utils_Hook
::singleton()->reset();
106 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
107 //$this->cleanupMailingTest();
111 public function testBasic() {
112 $this->createContactsInGroup(10, $this->_groupID
);
113 Civi
::settings()->add([
114 'mailerBatchLimit' => 2,
116 $this->callAPISuccess('mailing', 'create', $this->_params
);
117 $this->_mut
->assertRecipients([]);
118 $this->callAPISuccess('job', 'process_mailing', []);
119 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
123 * Test what happens when a contact is set to decesaed
125 public function testDecesasedRecepient() {
126 $contactID = $this->individualCreate(['first_name' => 'test dead recipeint', 'email' => 'mailtestdead@civicrm.org']);
127 $this->callAPISuccess('group_contact', 'create', [
128 'contact_id' => $contactID,
129 'group_id' => $this->_groupID
,
132 $this->createContactsInGroup(2, $this->_groupID
);
133 Civi
::settings()->add([
134 'mailerBatchLimit' => 2,
136 $mailing = $this->callAPISuccess('mailing', 'create', $this->_params
);
137 $this->assertEquals(3, $this->callAPISuccess('MailingRecipients', 'get', ['mailing_id' => $mailing['id']])['count']);
138 $this->_mut
->assertRecipients([]);
139 $this->callAPISuccess('Contact', 'create', ['id' => $contactID, 'is_deceased' => 1, 'contact_type' => 'Individual']);
140 $this->callAPISuccess('job', 'process_mailing', []);
141 // Check that the deceased contact is not found in the mailing.
142 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
147 * Test pause and resume on Mailing.
149 public function testPauseAndResumeMailing() {
150 $this->createContactsInGroup(10, $this->_groupID
);
151 Civi
::settings()->add([
152 'mailerBatchLimit' => 2,
154 $this->_mut
->clearMessages();
155 //Create a test mailing and check if the status is set to Scheduled.
156 $result = $this->callAPISuccess('mailing', 'create', $this->_params
);
157 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
158 $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']);
161 CRM_Mailing_BAO_MailingJob
::pause($result['id']);
162 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
163 $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']);
165 //Verify if Paused mailing isn't considered in process_mailing job.
166 $this->callAPISuccess('job', 'process_mailing', []);
167 //Check if mail log is empty.
168 $this->_mut
->assertMailLogEmpty();
169 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
170 $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']);
172 //Resume should set the status back to Scheduled.
173 CRM_Mailing_BAO_MailingJob
::resume($result['id']);
174 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
175 $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']);
177 //Execute the job and it should send the mailing to the recipients now.
178 $this->callAPISuccess('job', 'process_mailing', []);
179 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
183 * Test mail when in non-production environment.
186 public function testMailNonProductionRun() {
187 // Test in non-production mode.
189 'environment' => 'Staging',
191 $this->callAPISuccess('Setting', 'create', $params);
192 //Assert if outbound mail is disabled.
193 $mailingBackend = Civi
::settings()->get('mailing_backend');
194 $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config
::OUTBOUND_OPTION_DISABLED
);
196 $this->createContactsInGroup(10, $this->_groupID
);
197 Civi
::settings()->add([
198 'mailerBatchLimit' => 2,
200 $this->callAPISuccess('mailing', 'create', $this->_params
);
201 $this->_mut
->assertRecipients([]);
202 $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.");
204 // Test with runInNonProductionEnvironment param.
205 $this->callAPISuccess('job', 'process_mailing', ['runInNonProductionEnvironment' => TRUE]);
206 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
208 $jobId = $this->callAPISuccessGetValue('Job', [
210 'api_action' => "group_rebuild",
212 $this->callAPISuccess('Job', 'create', [
214 'parameters' => "runInNonProductionEnvironment=TRUE",
216 $jobManager = new CRM_Core_JobManager();
217 $jobManager->executeJobById($jobId);
219 //Assert if outbound mail is still disabled.
220 $mailingBackend = Civi
::settings()->get('mailing_backend');
221 $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config
::OUTBOUND_OPTION_DISABLED
);
223 // Test in production mode.
225 'environment' => 'Production',
227 $this->callAPISuccess('Setting', 'create', $params);
228 $this->callAPISuccess('job', 'process_mailing', []);
229 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
232 public function concurrencyExamples() {
235 // Launch 3 workers, but mailerJobsMax limits us to 1 worker.
240 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
241 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
242 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
243 // trampling effects.
245 'mailerBatchLimit' => 4,
246 'mailerJobsMax' => 1,
249 // 2 jobs which produce 0 messages
251 // 1 job which produces 4 messages
257 // Launch 3 workers, but mailerJobsMax limits us to 2 workers.
263 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
264 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
265 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
266 // trampling effects.
268 'mailerBatchLimit' => 5,
269 'mailerJobsMax' => 2,
273 // 1 job which produce 0 messages
275 // 2 jobs which produce 5 messages
282 // Launch 3 workers and saturate them (mailerJobsMax=3)
288 'mailerBatchLimit' => 6,
289 'mailerJobsMax' => 3,
293 // 3 jobs which produce 6 messages
300 // Launch 4 workers and saturate them (mailerJobsMax=0)
306 'mailerBatchLimit' => 6,
307 'mailerJobsMax' => 0,
311 // 3 jobs which produce 6 messages
313 // 1 job which produces 2 messages
320 // Launch 1 worker, 3 times in a row. Deliver everything.
327 'mailerBatchLimit' => 7,
331 // 1 job which produces 7 messages
333 // 1 job which produces 3 messages
335 // 1 job which produces 0 messages
342 // Launch 2 worker, 3 times in a row. Deliver everything.
349 'mailerBatchLimit' => 3,
353 // 3 jobs which produce 3 messages
355 // 1 job which produces 1 messages
357 // 2 jobs which produce 0 messages
364 // For two mailings, launch 1 worker, 5 times in a row. Deliver everything.
372 'mailerBatchLimit' => 6,
376 // x6 => x4+x2 => x6 => x2 => x0
377 // 3 jobs which produce 6 messages
379 // 1 job which produces 2 messages
381 // 1 job which produces 0 messages
392 * Setup various mail configuration options (eg $mailerBatchLimit,
393 * $mailerJobMax) and spawn multiple worker threads ($workers).
394 * Allow the threads to complete. (Optionally, repeat the above
395 * process.) Finally, check to see if the right number of
396 * jobs delivered the right number of messages.
398 * @param array $settings
399 * An array of settings (eg mailerBatchLimit, workers). See comments
400 * for $this->defaultSettings.
401 * @param array $expectedTallies
402 * A listing of the number cron-runs keyed by their size.
403 * For example, array(10=>2) means that there 2 cron-runs
404 * which delivered 10 messages each.
405 * @param int $expectedTotal
406 * The total number of contacts for whom messages should have
408 * @dataProvider concurrencyExamples
410 public function testConcurrency($settings, $expectedTallies, $expectedTotal) {
411 $settings = array_merge($this->defaultSettings
, $settings);
413 $this->createContactsInGroup($settings['recipients'], $this->_groupID
);
414 Civi
::settings()->add(CRM_Utils_Array
::subset($settings, [
420 for ($i = 0; $i < $settings['mailings']; $i++
) {
421 $this->callAPISuccess('mailing', 'create', $this->_params
);
424 $this->_mut
->assertRecipients([]);
427 for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++
) {
428 $apiCalls = $this->createExternalAPI();
429 $apiCalls->addEnv(['CIVICRM_CRON_HOLD' => $settings['lockHold']]);
430 for ($workerId = 0; $workerId < $settings['workers']; $workerId++
) {
431 $apiCalls->addCall('job', 'process_mailing', []);
434 $this->assertEquals($settings['workers'], $apiCalls->getRunningCount());
437 $allApiResults = array_merge($allApiResults, $apiCalls->getResults());
440 $actualTallies = $this->tallyApiResults($allApiResults);
441 $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r([
442 'expectedTallies' => $expectedTallies,
443 'actualTallies' => $actualTallies,
444 'apiResults' => $allApiResults,
446 $this->_mut
->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings']));
447 $this->assertEquals(0, $apiCalls->getRunningCount());
451 * Create contacts in group.
454 * @param int $groupID
455 * @param string $domain
457 public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') {
458 for ($i = 1; $i <= $count; $i++
) {
459 $contactID = $this->individualCreate(['first_name' => $count, 'email' => 'mail' . $i . '@' . $domain]);
460 $this->callAPISuccess('group_contact', 'create', [
461 'contact_id' => $contactID,
462 'group_id' => $groupID,
469 * Construct the list of email addresses for $count recipients.
473 * @param string $domain
474 * @param int $mailings
478 public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) {
480 for ($m = 0; $m < $mailings; $m++
) {
481 for ($i = $start; $i < ($start +
$count); $i++
) {
482 $recipients[][0] = 'mail' . $i . '@' . $domain;
488 protected function cleanupMailingTest() {
489 $this->quickCleanup([
491 'civicrm_mailing_job',
492 'civicrm_mailing_spool',
493 'civicrm_mailing_group',
494 'civicrm_mailing_recipients',
495 'civicrm_mailing_event_queue',
496 'civicrm_mailing_event_bounce',
497 'civicrm_mailing_event_delivered',
499 'civicrm_group_contact',
505 * Categorize results based on (a) whether they succeeded
506 * and (b) the number of messages sent.
508 * @param array $apiResults
510 * One key 'error' for all failures.
511 * A separate key for each distinct quantity.
513 protected function tallyApiResults($apiResults) {
515 foreach ($apiResults as $apiResult) {
516 $key = !empty($apiResult['is_error']) ?
'error' : $apiResult['values']['processed'];
517 $ret[$key] = !empty($ret[$key]) ?
1 +
$ret[$key] : 1;