Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD) ব্যবহার করে লাইব্রেরির ফাংশনালিটি তৈরি করা

যেহেতু এখন আমাদের সার্চ লজিকটি main ফাংশন থেকে আলাদা করে src/lib.rs-এ রাখা হয়েছে, তাই আমাদের কোডের মূল ফাংশনালিটির জন্য টেস্ট লেখা অনেক সহজ হয়ে গেছে। আমরা বিভিন্ন আর্গুমেন্ট দিয়ে সরাসরি ফাংশন কল করতে পারি এবং কমান্ড লাইন থেকে আমাদের বাইনারি কল না করেই রিটার্ন ভ্যালু পরীক্ষা করতে পারি।

এই বিভাগে, আমরা টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD) প্রক্রিয়া ব্যবহার করে minigrep প্রোগ্রামে সার্চিং লজিক যোগ করব। এর জন্য আমরা নিম্নলিখিত ধাপগুলো অনুসরণ করব:

  1. একটি টেস্ট লিখুন যা ফেইল করবে এবং এটি চালিয়ে নিশ্চিত হন যে এটি আপনার প্রত্যাশিত কারণেই ফেইল করছে।
  2. নতুন টেস্টটি পাস করানোর জন্য শুধুমাত্র প্রয়োজনীয় কোড লিখুন বা পরিবর্তন করুন।
  3. আপনি এইমাত্র যে কোড যোগ বা পরিবর্তন করেছেন তা রিফ্যাক্টর করুন এবং নিশ্চিত করুন যে টেস্টগুলো পাস করছে।
  4. ধাপ ১ থেকে পুনরাবৃত্তি করুন!

যদিও সফটওয়্যার লেখার অনেক পদ্ধতির মধ্যে এটি একটি, TDD কোড ডিজাইনকে চালিত করতে সাহায্য করতে পারে। যে কোডটি টেস্ট পাস করাবে, তা লেখার আগে টেস্টটি লিখে ফেললে পুরো প্রক্রিয়া জুড়ে হাই টেস্ট কভারেজ বজায় রাখতে সাহায্য করে।

আমরা সেই ফাংশনালিটির ইমপ্লিমেন্টেশন টেস্ট-ড্রাইভ করব যা ফাইলের কন্টেন্টে কোয়েরি স্ট্রিং খুঁজবে এবং কোয়েরির সাথে মেলে এমন লাইনের একটি তালিকা তৈরি করবে। আমরা এই ফাংশনালিটি search নামের একটি ফাংশনে যোগ করব।

একটি ফেইলিং টেস্ট লেখা (Writing a Failing Test)

src/lib.rs-এ, আমরা অধ্যায় ১১-এর মতো একটি tests মডিউল এবং একটি টেস্ট ফাংশন যোগ করব। টেস্ট ফাংশনটি search ফাংশনের প্রত্যাশিত আচরণ নির্দিষ্ট করবে: এটি একটি কোয়েরি এবং সার্চ করার জন্য টেক্সট নেবে এবং টেক্সট থেকে শুধুমাত্র সেই লাইনগুলোই রিটার্ন করবে যেগুলোতে কোয়েরিটি রয়েছে। লিস্টিং ১২-১৫ এই টেস্টটি দেখাচ্ছে।

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

// --snip--

#[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));
    }
}

এই টেস্টটি "duct" স্ট্রিংটি সার্চ করছে। আমরা যে টেক্সটটি সার্চ করছি তা তিন লাইনের, যার মধ্যে কেবল একটিতে "duct" রয়েছে (লক্ষ্য করুন যে ওপেনিং ডাবল কোটের পরে ব্যাকস্ল্যাশ Rust-কে বলে যে এই স্ট্রিং লিটারেলের শুরুতে একটি নিউলাইন ক্যারেক্টার যোগ না করতে)। আমরা assert করছি যে search ফাংশন থেকে রিটার্ন করা ভ্যালুতে শুধুমাত্র আমাদের প্রত্যাশিত লাইনটি রয়েছে।

যদি আমরা এই টেস্টটি চালাই, এটি বর্তমানে ফেইল করবে কারণ unimplemented! ম্যাক্রো "not implemented" মেসেজ দিয়ে প্যানিক করবে। TDD নীতি অনুসারে, আমরা একটি ছোট পদক্ষেপ নেব এবং টেস্টটি যাতে ফাংশন কল করার সময় প্যানিক না করে তার জন্য যথেষ্ট কোড যোগ করব। এর জন্য আমরা search ফাংশনটিকে সর্বদা একটি খালি ভেক্টর রিটার্ন করার জন্য ডিফাইন করব, যেমনটি লিস্টিং ১২-১৬-তে দেখানো হয়েছে। তাহলে টেস্টটি কম্পাইল হবে এবং ফেইল করবে কারণ একটি খালি ভেক্টর "safe, fast, productive." লাইনসহ একটি ভেক্টরের সাথে মিলবে না।

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

#[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));
    }
}```

</Listing>

এখন আসুন আলোচনা করি কেন আমাদের `search`-এর সিগনেচারে একটি সুস্পষ্ট লাইফটাইম `'a` ডিফাইন করতে হবে এবং সেই লাইফটাইমটি `contents` আর্গুমেন্ট এবং রিটার্ন ভ্যালুর সাথে ব্যবহার করতে হবে। [অধ্যায় ১০][ch10-lifetimes]<!-- ignore --> থেকে স্মরণ করুন যে লাইফটাইম প্যারামিটারগুলো নির্দিষ্ট করে যে কোন আর্গুমেন্টের লাইফটাইম রিটার্ন ভ্যালুর লাইফটাইমের সাথে সংযুক্ত। এই ক্ষেত্রে, আমরা নির্দেশ করছি যে রিটার্ন করা ভেক্টরে স্ট্রিং স্লাইস থাকবে যা `contents` আর্গুমেন্টের স্লাইসকে রেফারেন্স করে ( `query` আর্গুমেন্টকে নয়)।

অন্য কথায়, আমরা Rust-কে বলছি যে `search` ফাংশন দ্বারা রিটার্ন করা ডেটা তত সময় পর্যন্ত বেঁচে থাকবে, যত সময় `contents` আর্গুমেন্টে `search` ফাংশনে পাস করা ডেটা বেঁচে থাকবে। এটি গুরুত্বপূর্ণ! একটি স্লাইস দ্বারা রেফারেন্স করা ডেটা রেফারেন্সটি বৈধ হওয়ার জন্য অবশ্যই বৈধ হতে হবে; যদি কম্পাইলার ধরে নেয় যে আমরা `contents`-এর পরিবর্তে `query`-এর স্ট্রিং স্লাইস তৈরি করছি, তবে এটি তার সেফটি চেকিং ভুলভাবে করবে।

যদি আমরা লাইফটাইম অ্যানোটেশন ভুলে যাই এবং এই ফাংশনটি কম্পাইল করার চেষ্টা করি, আমরা এই এররটি পাব:

```console
$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:51
  |
1 | 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
  |
1 | 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` (lib) due to 1 previous error

Rust জানতে পারে না যে আউটপুটের জন্য আমাদের দুটি প্যারামিটারের মধ্যে কোনটি প্রয়োজন, তাই আমাদের এটি স্পষ্টভাবে বলতে হবে। লক্ষ্য করুন যে সাহায্যকারী টেক্সটটি সমস্ত প্যারামিটার এবং আউটপুট টাইপের জন্য একই লাইফটাইম প্যারামিটার নির্দিষ্ট করার পরামর্শ দেয়, যা ভুল! যেহেতু contents হলো সেই প্যারামিটার যেখানে আমাদের সমস্ত টেক্সট রয়েছে এবং আমরা সেই টেক্সটের যে অংশগুলো মেলে তা রিটার্ন করতে চাই, আমরা জানি যে শুধুমাত্র contents প্যারামিটারটিই লাইফটাইম সিনট্যাক্স ব্যবহার করে রিটার্ন ভ্যালুর সাথে সংযুক্ত হওয়া উচিত।

অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজে আপনাকে সিগনেচারে আর্গুমেন্টগুলোকে রিটার্ন ভ্যালুর সাথে সংযুক্ত করতে হয় না, কিন্তু এই অনুশীলনটি সময়ের সাথে সাথে সহজ হয়ে যাবে। আপনি এই উদাহরণটি অধ্যায় ১০-এর "Validating References with Lifetimes" বিভাগের উদাহরণগুলোর সাথে তুলনা করতে পারেন।

টেস্ট পাস করার জন্য কোড লেখা (Writing Code to Pass the Test)

বর্তমানে, আমাদের টেস্টটি ফেইল করছে কারণ আমরা সবসময় একটি খালি ভেক্টর রিটার্ন করি। এটি ঠিক করতে এবং search ইমপ্লিমেন্ট করতে, আমাদের প্রোগ্রামকে এই ধাপগুলো অনুসরণ করতে হবে:

  1. কন্টেন্টের প্রতিটি লাইনের মধ্যে দিয়ে ইটারেট (iterate) করা।
  2. লাইনটিতে আমাদের কোয়েরি স্ট্রিং আছে কিনা তা পরীক্ষা করা।
  3. যদি থাকে, তবে এটিকে আমরা যে ভ্যালুগুলো রিটার্ন করছি তার তালিকায় যুক্ত করা।
  4. যদি না থাকে, তবে কিছুই না করা।
  5. যে রেজাল্টগুলো ম্যাচ করে তার তালিকা রিটার্ন করা।

চলুন প্রতিটি ধাপ নিয়ে কাজ করা যাক, লাইন ইটারেট করা দিয়ে শুরু করি।

lines মেথড দিয়ে লাইন বরাবর ইটারেট করা

Rust-এর একটি সহায়ক মেথড আছে যা স্ট্রিং-এর লাইন-বাই-লাইন ইটারেশন পরিচালনা করে, যার সুবিধাজনক নাম lines, যা লিস্টিং ১২-১৭-তে দেখানো হয়েছে। মনে রাখবেন যে এটি এখনও কম্পাইল হবে না।

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

#[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));
    }
}

lines মেথড একটি iterator রিটার্ন করে। আমরা অধ্যায় ১৩-তে iterator নিয়ে গভীরভাবে আলোচনা করব, কিন্তু স্মরণ করুন যে আপনি লিস্টিং ৩-৫-এ iterator ব্যবহারের এই উপায়টি দেখেছেন, যেখানে আমরা একটি কালেকশনের প্রতিটি আইটেমের উপর কিছু কোড চালানোর জন্য একটি for লুপের সাথে একটি iterator ব্যবহার করেছি।

কোয়েরির জন্য প্রতিটি লাইন সার্চ করা

এরপরে, আমরা পরীক্ষা করব যে বর্তমান লাইনে আমাদের কোয়েরি স্ট্রিং আছে কিনা। সৌভাগ্যবশত, স্ট্রিং-এর contains নামে একটি সহায়ক মেথড আছে যা আমাদের জন্য এই কাজটি করে দেয়! search ফাংশনে contains মেথডের একটি কল যোগ করুন, যেমনটি লিস্টিং ১২-১৮-তে দেখানো হয়েছে। মনে রাখবেন যে এটি এখনও কম্পাইল হবে না।

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
        }
    }
}

#[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));
    }
}

এই মুহূর্তে, আমরা ফাংশনালিটি তৈরি করছি। কোডটি কম্পাইল করার জন্য, আমাদের ফাংশন সিগনেচারে যেমনটি নির্দেশ করেছিলাম, তেমন একটি ভ্যালু বডি থেকে রিটার্ন করতে হবে।

ম্যাচ করা লাইনগুলো সংরক্ষণ করা

এই ফাংশনটি শেষ করার জন্য, আমাদের ম্যাচ করা লাইনগুলো সংরক্ষণ করার একটি উপায় দরকার যা আমরা রিটার্ন করতে চাই। এর জন্য, আমরা for লুপের আগে একটি মিউটেবল ভেক্টর তৈরি করতে পারি এবং ভেক্টরে একটি line সংরক্ষণ করতে push মেথড কল করতে পারি। for লুপের পরে, আমরা ভেক্টরটি রিটার্ন করি, যেমনটি লিস্টিং ১২-১৯-এ দেখানো হয়েছে।

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
}

#[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));
    }
}

এখন search ফাংশনটি শুধুমাত্র সেই লাইনগুলো রিটার্ন করবে যেগুলোতে query রয়েছে এবং আমাদের টেস্ট পাস করা উচিত। চলুন টেস্টটি চালাই:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [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

আমাদের টেস্ট পাস হয়েছে, তাই আমরা জানি এটি কাজ করছে!

এই পর্যায়ে, আমরা টেস্টগুলো পাস করিয়ে রেখে একই ফাংশনালিটি বজায় রেখে সার্চ ফাংশনের ইমপ্লিমেন্টেশন রিফ্যাক্টর করার সুযোগ বিবেচনা করতে পারি। সার্চ ফাংশনের কোডটি খুব খারাপ নয়, তবে এটি iterator-এর কিছু দরকারী বৈশিষ্ট্যের সুবিধা নেয় না। আমরা অধ্যায় ১৩-তে এই উদাহরণে ফিরে আসব, যেখানে আমরা iterator বিস্তারিতভাবে অন্বেষণ করব এবং দেখব কীভাবে এটিকে উন্নত করা যায়।

এখন পুরো প্রোগ্রামটি কাজ করা উচিত! চলুন এটি চেষ্টা করে দেখি, প্রথমে এমন একটি শব্দ দিয়ে যা এমিলি ডিকিনসনের কবিতা থেকে ঠিক একটি লাইন রিটার্ন করবে: frog

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [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` profile [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` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

চমৎকার! আমরা একটি ক্লাসিক টুলের নিজস্ব মিনি সংস্করণ তৈরি করেছি এবং অ্যাপ্লিকেশন কীভাবে গঠন করতে হয় সে সম্পর্কে অনেক কিছু শিখেছি। আমরা ফাইল ইনপুট এবং আউটপুট, লাইফটাইম, টেস্টিং এবং কমান্ড লাইন পার্সিং সম্পর্কেও কিছু শিখেছি।

এই প্রজেক্টটি শেষ করার জন্য, আমরা সংক্ষেপে দেখাব কীভাবে এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করতে হয় এবং কীভাবে স্ট্যান্ডার্ড এরর-এ প্রিন্ট করতে হয়, উভয়ই কমান্ড লাইন প্রোগ্রাম লেখার সময় দরকারী।