اذهب إلى المحتوى

كتابة برنامج سطر أوامر بلغة رست: اختبار البرنامج


Naser Dakhel

بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في المقال التالي كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ونتطرّق في هذا المقال إلى اختبار البرنامج.

تطوير عمل المكتبة باستخدام التطوير المقاد بالاختبار test-driven

الآن، وبعد استخراجنا لمعظم منطق البرنامج إلى الملف src/lib.rs، يبقى لدينا منطق الحصول على الوسطاء والتعامل مع الأخطاء في src/main.rs، ومن الأسهل كتابة الاختبارات في هذه الحالة، إذ ستركّز الاختبارات على منطق شيفرتنا البرمجية الأساسية. يمكننا استدعاء الدوال مباشرةً باستخدام مختلف الوسطاء arguments والتحقق من القيمة المعادة دون الحاجة لاستدعاء ملفنا التنفيذي من سطر الأوامر.

نُضيف في هذا القسم منطق البحث إلى البرنامج "minigrep" باستخدام التطوير المُقاد بالاختبار test-driven development -أو اختصارًا TDD- باتباع الخطوات التالية:

  1. كتابة اختبار يفشل وتنفيذه للتأكد من أنه يفشل فعلًا للسبب الذي تتوقعه.
  2. كتابة شيفرة برمجية أو التعديل على شيفرة برمجية موجودة مسبقًا لجعل الاختبار الجديد ينجح.
  3. إعادة بناء التعليمات البرمجية المُضافة أو المُعدّلة للتأكد من أن الاختبارات ستنجح دومًا.
  4. كرّر الأمر مجدّدًا بدءًا من الخطوة 1.

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

سنختبر تطبيق الخاصية التي ستبحث عن السلسلة النصية المُدخلة في محتويات الملف وتعطينا قائمةً من الأسطر تحتوي على حالات التطابق، ثمّ سنضيف هذه الخاصية في دالة تدعى search.

كتابة اختبار فاشل

دعنا نتخلص من تعليمات println!‎ من الملفين "src/lib.rs" و "src/main.rs" التي كنا نستخدمها لتفقُّد سلوك البرنامج ولن نحتاج إليها بعد الآن، ثم نضيف الوحدة tests في الملف "src/lib.rs" مع دالة اختبار كما فعلنا في مقال سابق. تحدد دالة الاختبار السلوك الذي نريده من الدالة search ألا وهو: ستأخذ الدالة سلسلةً نصيةً محددةً ونصًا تبحث فيه وستُعيد السطور التي تحتوي على تطابق. توضح الشيفرة 15 هذا الاختبار، إلا أنها لن تُصرَّف بنجاح بعد.

اسم الملف: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

الشيفرة 15: إنشاء اختبار فاشل للدالة search التي كنا نتمنى الحصول عليها

يبحث هذا الاختبار عن السلسلة النصية "duct"، إذ يتألف النص الذي نبحث فيه من ثلاثة أسطر ويحتوي واحدٌ منها فقط السلسلة النصية "duct" (يخبر الخط المائل العكسي backslash بعد علامتي التنصيص المزدوجتين رست بعدم إضافة محرف سطر جديد في بداية محتوى السلسلة النصية المجردة). نتأكد أن القيمة المُعادة من الدالة search تحتوي فقط على السطر الذي نتوقعه.

لا يمكننا تنفيذ هذا الاختبار بعد ورؤيته يفشل لأن الاختبار لا يُصرَّف، والسبب في ذلك هو أن الدالة search غير موجودة بعد. وفقًا لمبادئ التطوير المُقاد بالاختبار، علينا إضافة القليل من الشيفرة البرمجية بحيث يمكننا تصريف الاختبار وتنفيذه بإضافة تعريف الدالة search التي تُعيد شعاعًا vector فارغًا دومًا كما هو موضح في الشيفرة 16، ومن ثم يجب أن يُصرَّف الاختبار ويفشل لعدم مطابقة الشعاع الفارغ للشعاع الذي يحتوي السطر "safe, fast, productive.".

اسم الملف: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

الشيفرة 16: تعريف الدالة search باستخدام شيفرة برمجية قصيرة بحيث يُصرَّف الاختبار

لاحظ أننا بحاجة إلى تعريف دورة حياة lifetime صراحةً تدعى ‎'a في بصمة الدالة search واستخدام دورة الحياة في الوسيط contents والقيمة المُعادة. تذكر أننا ذكرنا في مقال سابق أن معاملات دورة الحياة تحدد أي دورات حياة الوسطاء متصلة بدورة حياة القيمة المُعادة، وفي هذه الحالة فإننا نحدد أن الشعاع المُعاد يجب أن يحتوي على شرائح سلسلة نصية string slices تمثل مرجعًا لشرائح الوسيط contents بدلًا من الوسيط query.

بكلمات أخرى، نخبر رست بأن البيانات المُعادة من الدالة search ستعيش طالما تعيش البيانات المُمرّرة إلى الدالة search في الوسيط contents. يجب أن تكون الشرائح المُستخدمة مثل مراجع للبيانات صالحة حتى يكون المرجع صالحًا، إذ سيكون التحقق من الأمان خاطئًا لو افترض المصرف أننا نُنشئ شرائح سلاسل نصية من query بدلًا من contents.

نحصل على الخطأ التالي إذا نسينا توصيف دورات الحياة وجرّبنا تصريف هذه الدالة:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
  --> src/lib.rs:28:51
   |
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
   |                      ----            ----         ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
   |              ++++         ++                 ++              ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` due to previous error

لا يمكن لرست معرفة أي الوسيطين نحتاج، لذا يجب أن نصرح عن ذلك مباشرةً. نعلم أن contents هو الوسيط الذي يجب أن يُربط مع القيمة المُعادة باستخدام دورة الحياة وذلك لأنه الوسيط الذي يحتوي على كامل محتوى الملف النصي الذي نريد أن نعيد أجزاءً متطابقةً منه.

لا تتطلب لغات البرمجة الأخرى ربط الوسطاء للقيمة المعادة في بصمة الدالة، إلا أنك ستعتاد على ذلك مع الممارسة. ننصحك بمقارنة هذا المثال مع مثال موجود في مقال سابق.

دعنا ننفذ الاختبار:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... FAILED

failures:

---- tests::one_result stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

عظيم، فشل الاختبار كما توقعنا. دعنا نجعل الاختبار ينجح الآن.

كتابة شيفرة برمجية لاجتياز الاختبار

يفشل اختبارنا حاليًا لأننا نُعيد دائمًا شعاعًا فارغًا، وعلى برنامجنا اتباع الخطوات التالية لتصحيح ذلك وتطبيق search:

  • المرور على سطور محتوى الملف.
  • التحقق ما إذا كان السطر يحتوي على السلسلة النصية التي نبحث عنها.
  • إذا كان هذا الأمر محققًا: نضيف السطر إلى قائمة القيم التي سنعيدها.
  • إن لم يكن محققًا: لا نفعل أي شيء.
  • نُعيد قائمة الأسطر التي تحتوي على تطابق مع السلسلة النصية التي نبحث عنها.

لنعمل على كل خطوة بالتدريج، بدءًا من المرور على الأسطر على الترتيب.

المرور على الأسطر باستخدام التابع lines

توفر لنا رست تابعًا مفيدًا للتعامل مع السلاسل النصية سطرًا تلو الآخر بصورةٍ سهلة وهو تابع lines، ويعمل التابع بالشكل الموضح في الشيفرة 17. انتبه إلى أن الشيفرة 17 لن تُصرَّف بنجاح.

اسم الملف: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // أجرِ بعض العمليات على ‫line
    }
}

الشيفرة 17: المرور على أسطر الوسيط contents

يُعيد التابع lines مكرّرًا iterator، وسنتحدث عن المكررات فيما بعد؛ تذّكر أنك رأيت هذه الطريقة باستعمال المكررات في الشيفرة 5 من فصل سابق عندما استخدمنا حلقة for مع مكرر لتنفيذ شيفرة برمجية على كل عنصر من عناصر التجميعة collection.

البحث عن الاستعلام في كل سطر

الآن نبحث فيما إذا كان السطر يحتوي على السلسلة النصية المحددة بالاستعلام، وتحتوي السلاسل النصية لحسن الحظ على تابع مفيد يدعى contains يفعل هذا نيابةً عنّا. أضف استدعاءً للتابع contains في الدالة search كما هو موضح في الشيفرة 18. لاحظ أن هذه الشيفرة البرمجية لن تُصرَّف بعد.

اسم الملف: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

الشيفرة 18: إضافة ميزة البحث عن السلسلة النصية الموجودة في الوسيط query داخل السطر

بدأنا ببناء وظيفة البرنامج الأساسية الآن، ولتصريف البرنامج بنجاح نحن بحاجة لإعادة قيمة من متن الدالة كما قلنا أننا سنفعل في بصمة الدالة.

تخزين الأسطر المطابقة

أخيرًا يجب علينا استخدام طريقة لتخزين الأسطر المُطابقة التي نريد أن نُعيدها من الدالة، ونستخدم لذلك شعاعًا متغيّرًا mutable vector قبل الحلقة for ونستدعي التابع push لتخزين line في الشعاع، ونُعيد هذا الشعاع بعد انتهاء الحلقة for كما هو موضح في الشيفرة 19.

اسم الملف: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

الشيفرة 19: تخزين الأسطر المتطابقة بحيث نستطيع إعادتهم من الدالة

الآن يجب أن تُعيد الدالة search فقط الأسطر التي تحتوي على الوسيط query مما يعني أن اختبارنا سينجح. دعنا ننفّذ الاختبار:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

نجح الاختبار، لذا فالدالة تعمل.

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

استخدام الدالة search في الدالة run

الآن وبعد عمل الدالة search وتجربتها بنجاح، نحتاج إلى استدعاء search من الدالة run، وتمرير قيمة config.query و contents التي تقرأهما run من الملف إلى الدالة search. تطبع الدالة run كل سطر مُعاد من الدالة search:

اسم الملف: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

ما زلنا نستخدم الحلقة for لإعادة كل سطر من search وطباعته.

يجب أن يعمل كامل البرنامج الآن. دعنا نجرّبه أولًا بكلمة "الضفدع frog" التي ينبغي أن تُعيد سطرًا واحدًا بالتحديد من قصيدة ايميلي ديكنسون Emily Dickinson:

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

عظيم. دعنا نجرب كلمةً يُفترض أنها موجودة في عدة أسطر مثل "body":

$ cargo run -- body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

وأخيرًا، دعنا نتأكد من أننا لن نحصل على أي سطر عندما تكون الكلمة غير موجودة ضمن أي سطر في القصيدة مثل الكلمة "monomorphization":

$ cargo run -- monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

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

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

ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...