لإعادة تشغيل مدخلات اختبار Swift العشوائية على جهاز Mac، مرّر بذرة صريحة إلى مولّد جديد لكل حالة اختبار. ثبّت المولّد وربط المدخلات وترتيب السحب، ثم سجّل البذرة مع كل إخفاق.

يعرض هذا الدليل التطبيقي حزمة Swift محلية صغيرة تتحقق من عملية حسابية اصطناعية. ويوفّر مثالًا كاملًا باستخدام Swift Testing وبديلًا باستخدام XCTest. تأتي جميع بيانات الاختبار من حسابات محلية؛ ولا تتضمن الحزمة أي تبعيات خارجية.

ما الذي تجعل البذرة قابلًا للتكرار فعلًا؟

هناك مصدران منفصلان للتغيّر: القيم التي يولّدها الاختبار والترتيب الذي يجدول به مشغّل الاختبارات الاختبارات. تتحكم بذرة يحددها التطبيق في المصدر الأول فقط عندما تأتي كل قيمة مولّدة من المولّد المقابل. توثّق Apple أن العمليات العشوائية المعتادة تستخدم افتراضيًا SystemRandomNumberGenerator ذي البذرة التلقائية. ولا يقبل المُهيّئ الموثق له بذرة. SystemRandomNumberGenerator

يوفّر Xcode أيضًا ترتيب تنفيذ عشوائيًا لكشف التبعيات بين الاختبارات. توثّق خطة الاختبار الخاصة به ترتيب التنفيذ الأبجدي أو العشوائي، لكنها لا توثّق بذرة إعادة تشغيل يحددها المستخدم لهذا الإعداد. لذا فإن المسار المدعوم المستخدم هنا هو مولّد مدخلات ذي بذرة صريحة داخل اختبارات مستقلة. ولا يضمن إعادة إنتاج جدولة المشغّل. تحسين تقييم الشيفرة بتنظيم الاختبارات في خطط اختبار

استخدم وسيلة المساعدة هذه لاتخاذ القرار قبل تعديل اختبار غير مستقر:

ما الذي يتغير بين مرات التشغيل؟ ما الذي ينبغي التحكم فيه؟
المدخلات الرقمية المولّدة البذرة، وتنفيذ المولّد، وربط المدخلات، وترتيب السحب
النتائج بعد تشغيل اختبار آخر الحالة القابلة للتعديل والتهيئة لكل اختبار
النتائج أثناء التنفيذ المتزامن فقط الموارد المشتركة القابلة للتعديل والعمل المتداخل
المدخلات بعد تحديث سلسلة الأدوات تنفيذ ربط المدخلات والمدخلات الفعلية المسجلة

يدعم Swift صراحةً المولّدات المخصصة عبر RandomNumberGenerator، والعملية المطلوبة فيه هي mutating next() التي تعيد UInt64. وتذكر وثائق المصدر في مشروع Swift أن توفير تسلسلات قابلة للتكرار للاختبار أحد أسباب إتاحة مولّد يقبل بذرة. Random.swift

أنشئ حزمة اختبار محلية صغيرة

استخدم جهاز Mac عليه Xcode وسلسلة أدوات Swift 6 أو أحدث. تحقّق من سلسلة الأدوات المحددة لسطر الأوامر في Terminal، ثم أنشئ دليلًا جديدًا وهيّئ حزمة مكتبة:

swift --version
mkdir SeedReplay
cd SeedReplay
swift package init --type library

تختبر اختبارات التكامل في مشروع Swift إنشاء حزم المكتبات باستخدام swift package init --type library. يتضمن Swift 6 إطار Swift Testing مباشرةً، لذا لا يحتاج هذا المثال إلى تبعية حزمة من أجل import Testing. يشغّل Swift Package Manager اختبارات Swift Testing واختبارات XCTest معًا عند وجودها. اختبار تكامل تهيئة حزمة Swift، الإعلان عن Swift 6

احتفظ ببيان الحزمة ومصدر المكتبة اللذين أُنشئا. واستبدل ملف الاختبار النموذجي المُنشأ بملفات الاختبار أدناه، وكلها داخل Tests/SeedReplayTests. توجد الدالة الحسابية في ملف دعم الاختبارات ليكون هذا العرض مكتفيًا بذاته. استخدم الدالة الفعلية قيد الاختبار في مشروع موجود.

عرّف عقد البذرة وتوليد المدخلات

أنشئ Tests/SeedReplayTests/ReplaySupport.swift بالمحتوى الكامل التالي:

import Foundation

// Local synthetic test-data generator.
// Mutable value state: give each test case its own instance.
// Its output sequence is part of this example's replay contract.
struct ReplayGenerator: RandomNumberGenerator {
    private var state: UInt64

    init(seed: UInt64) {
        state = seed
    }

    mutating func next() -> UInt64 {
        state &+= 0x9E3779B97F4A7C15
        var value = state
        value = (value ^ (value >> 30)) &* 0xBF58476D1CE4E5B9
        value = (value ^ (value >> 27)) &* 0x94D049BB133111EB
        return value ^ (value >> 31)
    }
}

enum ReplayConfigurationError: Error {
    case invalidSeed(String)
}

func replaySeeds() throws -> [UInt64] {
    guard let text = ProcessInfo.processInfo.environment["TEST_SEED"] else {
        return [0, 42, 2026]
    }

    guard !text.isEmpty,
          text.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }),
          let seed = UInt64(text) else {
        throw ReplayConfigurationError.invalidSeed(text)
    }

    return [seed]
}

func generatedInputs(seed: UInt64) -> [Int] {
    var generator = ReplayGenerator(seed: seed)
    return (0..<256).map { _ in
        // Exactly one draw per input; retain the upper ten bits.
        Int(generator.next() >> 54)
    }
}

func triangularNumber(_ n: Int) -> Int {
    precondition((0...1023).contains(n))
    return n * (n + 1) / 2
}

func referenceTotal(_ n: Int) -> Int {
    precondition((0...1023).contains(n))
    return (0...n).reduce(0, +)
}

يقبل الإعداد قيمة UInt64 عشرية، بما فيها الصفر. وعند عدم تعيين TEST_SEED، تُختار ثلاث بذور صريحة؛ أما القيمة الفارغة أو السالبة أو غير الرقمية أو التي تتجاوز النطاق فتؤدي إلى خطأ. وهكذا لا يمكن أن تؤدي كتابة بذرة خاطئة بصمت إلى اختيار تشغيل مختلف.

يستخدم المولّد حسابًا صريحًا مع التفاف القيم. يأخذ كل مدخل مولّد أعلى عشرة بتات من خرج واحد، فتكون القيم من 0 إلى 1023. وقد ثُبّت هذا الربط عمدًا في الشيفرة. وتظل العملية الحسابية صغيرة: أكبر نتيجة متوقعة هي 523776. هذه وصفة مدخلات اصطناعية محدودة، ولا تدّعي تحقيق تغطية إحصائية للاختبارات.

لماذا نتجنب Int.random(in:using:) هنا؟ تدعم هذه الواجهة مولّدًا مزودًا وتفيد عند إعادة التشغيل باستخدام سلسلة أدوات ثابتة. لكن Apple تحذّر صراحةً من أن خوارزمية توليد القيم قد تتغير بين إصدارات Swift، حتى مع مولّد ينتج التسلسل الأساسي نفسه. وتزيل كتابة هذا المثال لربط صغير خاص به هذا الاعتماد بعينه. Int.random(in:using:)

شغّل إصدار Swift Testing وأعِد تشغيله

أنشئ Tests/SeedReplayTests/SeedReplayTests.swift:

import Testing

@Test(arguments: try replaySeeds())
func triangularTotals(seed: UInt64) {
    print("triangularTotals seed=\(seed) recipe=upper10-v1 count=256")

    for (index, n) in generatedInputs(seed: seed).enumerated() {
        let actual = triangularNumber(n)
        let expected = referenceTotal(n)

        #expect(
            actual == expected,
            "seed=\(seed) case=\(index) n=\(n) actual=\(actual) expected=\(expected)"
        )

        if actual != expected {
            return
        }
    }
}

@Test
func replayRecipeIsStable() {
    var generator = ReplayGenerator(seed: 0)
    let firstWord = generator.next()
    #expect(firstWord == 0xE220A8397B1DCDAF)

    let firstInputs = Array(generatedInputs(seed: 0).prefix(3))
    #expect(firstInputs == [904, 441, 27])
}

يجعل الاختبار ذو المعلمات كل بذرة حالة اختبار مستقلة. توثّق Apple أن arguments: يمرّر عناصر المجموعة إلى استدعاءات الاختبار ويسمح بتعابير قد ترمي أخطاء. وهكذا يظل اختيار البذرة ظاهرًا في نموذج الاختبار. ويحدد فهرس الحلقة الصريح بعد ذلك مدخلًا مولّدًا بعينه ضمن تلك البذرة. تنفيذ الاختبارات ذات المعلمات

شغّل المجموعة الافتراضية، ثم أعد تشغيل إحدى البذور مرتين:

swift test
TEST_SEED=42 swift test
TEST_SEED=42 swift test

ينبغي أن تولّد هاتان المرتان المدخلات الرقمية نفسها عند عدم تغيير المصدر. بالنسبة إلى البذرة 42، تكون المدخلات الثلاثة الأولى 759 و163 و285. يختبر الاختبار 256 مدخلًا لهذه البذرة. ويبقى اختبار الثبات المنفصل مثبتًا عمدًا على البذرة صفر: فهو يتحقق من المولّد والربط بصرف النظر عن بذرة الاختبار الحسابي المحددة.

النتيجة المتوقعة من التنفيذ المقدم هي اجتياز جميع التأكيدات. لم تكن Swift متاحة في بيئة البحث، لذا فُحصت ملفات Swift بدلًا من تجميعها. وتحقق حساب مستقل بلغة Python من مقارنات الحساب الـ768 للبذور الافتراضية الثلاث والقيم الأولية المعروضة؛ ولا يغني هذا التحقق عن تجميع حزمة Swift.

تدرّب على إعادة تشغيل خطأ حسابي متعمد

لمعاينة مسار التشخيص، استبدل مؤقتًا تعبير الإرجاع في triangularNumber(_:) بهذا المقتطف الموسوم:

// Temporary replacement inside triangularNumber(_:).
return n * n / 2

شغّل TEST_SEED=42 swift test مرة أخرى. أول مدخل مولّد هو 759. يعطي التعبير المعدّل 288040؛ أما المجموع المرجعي فهو 288420. وتحدد رسالة التأكيد البذرة 42، وفهرس الحالة 0، والمدخل 759. ويوقف الإرجاع المبكر هذا الاستدعاء بعد أول عدم تطابق مسجّل، ليظل التشخيص مركزًا.

أعِد return n * (n + 1) / 2 وشغّل الأمر نفسه مرة أخرى. أبقِ البذرة ثابتة أثناء تقييم الإصلاح. بعد أن يكشف مدخل مولّد عن عيب مفيد، احتفظ بهذا المدخل الفعلي أيضًا في اختبار انحدار مسمّى؛ فسيظل مفهومًا حتى إذا تغيّرت وصفة التوليد لاحقًا.

طبّق دورة الحياة نفسها على XCTest

في هدف يعتمد على XCTest، احتفظ بـReplaySupport.swift واستخدم ملف الاختبار الكامل التالي بدلًا من ملف Swift Testing. سمّه Tests/SeedReplayTests/SeedReplayXCTests.swift:

import XCTest

final class SeedReplayXCTests: XCTestCase {
    func testTriangularTotals() throws {
        for seed in try replaySeeds() {
            print("testTriangularTotals seed=\(seed) recipe=upper10-v1 count=256")

            for (index, n) in generatedInputs(seed: seed).enumerated() {
                let actual = triangularNumber(n)
                let expected = referenceTotal(n)

                XCTAssertEqual(
                    actual,
                    expected,
                    "seed=\(seed) case=\(index) n=\(n)"
                )

                if actual != expected {
                    return
                }
            }
        }
    }

    func testReplayRecipeIsStable() {
        var generator = ReplayGenerator(seed: 0)
        XCTAssertEqual(generator.next(), 0xE220A8397B1DCDAF)
        XCTAssertEqual(
            Array(generatedInputs(seed: 0).prefix(3)),
            [904, 441, 27]
        )
    }
}

تنطبق أوامر Terminal نفسها. كل استدعاء إلى generatedInputs(seed:) ينشئ مولّدًا جديدًا، لذا لا يستهلك أي اختبار تدفق الأرقام العشوائية الخاص باختبار آخر. ولا توجد حالة مولّد يلزم تنظيفها بعد الاختبار.

إذا كان تجهيز XCTest قائمًا يخزّن مولّدًا كخاصية، فأعد ضبطه قبل كل طريقة اختبار. توثّق Apple طرق إعداد لكل اختبار وطرق إعداد للصنف للحالة المشتركة بين الاختبارات في الصنف. وينبغي تهيئة تدفق عشوائي قابل للتعديل لكل اختبار عندما يلزم أن تعيد الاختبارات التشغيل بصورة مستقلة. ولا يُعتمد على إعداد حالة الاختبار التالي في التنظيف بعد الاختبار، لأن التنظيف غير مضمون بعد حدوث انهيار. إعداد الحالة وتنظيفها في الاختبارات

أبقِ جدولة Xcode منفصلة عن إعادة تشغيل المدخلات

لتشغيل هذه الحزمة من Xcode، افتح Package.swift الخاص بها. في مشروع يستخدم خطة اختبار، اختر Product > Scheme > Edit Test Plan، وحدد الإعداد الملائم، ثم أضف TEST_SEED مع القيمة العشرية 42 ضمن Environment Variables. شغّل عبر Product > Test، وتحقق من البذرة المطبوعة في مخرجات الاختبار. توثّق Apple إعداد بيئة خطة الاختبار واختيار الخطة النشطة قبل تشغيل الاختبارات. تحسين تقييم الشيفرة بتنظيم الاختبارات في خطط اختبار

تحقق من البذرة المطبوعة كلما بدّلت بين Terminal وXcode. فالمتغير المحدد لأمر واحد في Terminal ليس إعدادًا لخطة اختبار Xcode. ويبيّن سطر السجل الإعداد الذي قرأته عملية الاختبار فعليًا.

يشغّل Swift Testing الاختبارات بالتوازي افتراضيًا. ويمكن لسمة .serialized تسلسل الحالات ضمن اختبار ذي معلمات أو الاختبارات ضمن مجموعة، لكنها لا تتحكم في الاختبارات غير المرتبطة. وتوثّق Apple أيضًا الخيار swift test --no-parallel لتعطيل التوازي على مستوى عام. ولا يوفر أي من هذين الخيارين بذرة للمدخلات. ولا يحتاج هذا المثال إلى التسلسل لأن لكل حالة مولّدها ومدخلاتها الرقمية الخاصة. تشغيل الاختبارات بالتسلسل أو بالتوازي

إذا اجتاز اختبارٌ منفردًا لكنه أخفق عند تشغيله مع اختبارات أخرى رغم تطابق المدخلات المولّدة، فتحقّق من الحالة المشتركة. وقد يفيد التنفيذ التسلسلي في فحص العمل المتداخل، لكن البذرة لا تعيد ضبط الكائنات القابلة للتعديل الخاصة باختبار آخر ولا تعيد إنتاج جدول تنفيذ متزامن كامل.

ما الذي ينبغي الاحتفاظ به مع بذرة الإخفاق؟

احتفظ بمعلومات كافية لإعادة إنشاء تسلسل المدخلات:

  • اسم الاختبار والبذرة العشرية.
  • إصدار المولّد والربط، وهما هنا upper10-v1.
  • فهرس الحالة والمدخل الفعلي الواردان في رسالة التأكيد.
  • مراجعة المصدر وإصدار Swift وإصدار Xcode المحدد.
  • عدد المدخلات وأي تغييرات في كيفية استهلاك السحوبات العشوائية.

في هذا المثال، تؤدي إضافة سحب من المولّد قبل الحلقة إلى إزاحة كل مدخل تالٍ. ويؤدي تغيير مقدار الإزاحة إلى تغيير الربط. أما زيادة عدد المدخلات فتحافظ على البادئة الموجودة ما دام المولّد والربط لم يتغيرا. وهذه نتائج تترتب على عقد إعادة التشغيل في الشيفرة.

احتفظ بمجموعة صغيرة من البذور الثابتة للتشغيل المعتاد، وجرّب بذورًا صريحة إضافية أثناء الاستكشاف، واحفظ مدخلات انحدار فعلية عندما تكشف عن عيب. تبدأ إعادة التشغيل المفيدة بالقيم نفسها وحالة ابتدائية معروفة؛ وتصبح البذرة موثوقة لأن الاختبار يتحكم في كيفية استهلاكها.

المصادر