exit() تستهای موازی رو بیصدا میشکنه
خلاصهٔ کاملتر
نویسنده میگه تو اجرای موازی تستهاشون یه WorkerCrashedException از ParaTest بالا میاومد که عملاً پیامی نداشت: نه اسم تستی که اجرا میشد، نه دلیل، نه سرنخی برای گشتن. هر فلگ تشخیصی هم که امتحان کرد، از --display-errors تا --display-warnings، هیچ فرقی نکرد.
برای فهمیدن دلیلش اول رفته سراغ خود runner. اجراکنندهٔ موازی Pest که روی ParaTest سواره، چند پروسهٔ جدا PHP بالا میاره و هر کدوم از یه صف فایل تست برمیدارن. هماهنگی از طریق فایلسیستمه: ورکرها نتیجه رو تو status file مینویسن و پروسهٔ پدر همون فایلها رو مرتب میخونه تا پیشرفت رو بفهمه.
وقتی یه ورکر ناگهانی میمیره، isRunning() میشه false و پدر همون WorkerCrashedException رو پرتاب میکنه. ولی اون استثنا فقط چیزی رو داره که ورکر فرصت کرده بود تو stdout بنویسه، که وقتی پروسه یهو قطع شده معمولاً هیچیه. این فرق تستیه که fail میشه با ورکریه که میمیره: تست fail نتیجهاش رو مینویسه، ورکر مرده فقط ناپدید میشه.
مقصر یه متد تو موتور قدیمی قیمتگذاریشون بود که اگه ردیف نرخ رو تو دیتابیس پیدا نمیکرد، به جای پرتاب استثنا exit() صدا میزد با پیام «نرخ پایه پیدا نشد». تو حالت تکپروسهای این کار کل PHPUnit رو میکشه ولی حداقل پیام رو تو ترمینال میبینی. تو حالت موازی فقط ورکر میمیره و پیام هم میره تو stdout ورکر، جایی که پدر برای تشخیص خطا سراغش نمیره.
فلگهای verbose هم به همین دلیل بیفایده بودن: اون فلگها به فریمورک میگن دربارهٔ تستهایی که به حالت نهایی رسیدن جزئیات بیشتری نشون بده، ولی exit() کل این مسیر رو دور میزنه. تست تموم نمیشه، PHPUnit نتیجهای ثبت نمیکنه، پس خطایی وجود نداره که نمایش داده بشه. یعنی exit() خطا رو پنهان نمیکنه، کاملاً حذفش میکنه.
راهحل ساده بود: عوض کردن exit() با پرتاب یه استثنا.
if (!empty($rate_row)) {
return $rate_row->rate;
} else {
throw new RuntimeException('Cannot find base rate in database.');
}استثنا بخشی از چرخهٔ عادی تسته، پس PHPUnit میگیرش، به عنوان error ثبتش میکنه، نتیجه تو status file نوشته میشه و پدر با stack trace کامل جمعش میکنه. ورکر هم نمیمیره؛ تست رو با خطا تموم میکنه و میره سر بعدی. به گفتهٔ نویسنده بعد از این تغییر، خطاهایی که کل عمر runner موازی پنهان بودن تو چند ثانیه دیده شدن و یه زنجیره مشکل ثانویه هم لو رفت، مثل return خالی تو مسیرهای لبه و ناسازگاری storage engine دیتابیس که rollback تراکنش تست رو خراب میکرد.
نکات کلیدی:
- پیام exit() به stdout ورکر میره، ولی پروسهٔ پدر فقط status file رو میخونه
- فلگهای --display-errors و --display-warnings فقط چیزی رو نشون میدن که فریمورک دیده باشه
- جایگزینی exit() با RuntimeException باعث میشه خطا با شماره خط و stack trace کامل ثبت بشه
- هر توقف سخت پروسه مثل exit() و die() و segfault تو حالت موازی رفتار کاملاً متفاوتی داره
- توصیهٔ نویسنده: قبل از موازی کردن یه کدبیس میراثی، با grep -rn 'exit(' app/ دنبال هر exit تو پروژه بگرد




