Commit | Line | Data |
---|---|---|
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 | */ |
43 | class 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 | } |