TechFULの中の人

TechFULスタッフ・エンジニアによる技術ブログ。IT関連のことやTechFULコーディングバトルの超難問の深掘り・解説などを紹介

テストケースが正解にならない時のTechFUL上でのデバッグ方法

こんにちは! 問題管理を担当しているTechFULエンジニアです!

TechFUL問題に挑戦している皆さんから、「サンプルケースは通るのにテストケースは通らない。テストケースは非公開になっているため手詰まりになってしまった。」というご意見を耳にします。 この記事では、そうした状況になった際にプログラムのバグを見つける方法の1つをご説明します。

デバッグ方法の基本

この記事では、以下の問題を例に説明します。(この問題は記事用に作成した問題のため、一般には公開されていません。)

通常、バグが起きた際はどのようなケースで落ちるのか、考えられる様々なケースを試してみます。 TechFULの公式問題は「制約」の欄に想定される入力値の情報が書かれていますので、そこを参考にケースを考えましょう。 (ちなみに、公式問題の場合は「制約」に違反するケースは入力として与えられることはありませんので、制約外のケースは考えなくて大丈夫です。)

考えられるケースと言っても、たくさんのパターンがあり過ぎてどのようなケースを考えれば良いのか分からないという場合は、まずは「極端なケース」を試してみましょう。

例えば...

  • 入力の最大値
  • 入力の最小値
  • 文字列の場合は全部同じ文字のケースや1文字、0文字のケース
  • 条件分岐がある場合は条件の境目となるケース

などです。

どのようなケースを考えたらよいかは練習を積むとだんだんと身についてきますので、ぜひいろんな問題に挑戦してみてください!

この問題の場合、制約は以下のように書かれています。

この制約ですと、例えば以下のようなケースが考えられます。

  • A=1, B=1のケース(最小ケース)
  • A=1000000000, B=1000000000000000000(最大ケース)
  • A=1, B=99 (Noになる境目のケース)
  • A=1, B=100 (Yesになる境目のケース)
  • A=1000000000, B=2000000000 (Noになるケースのうち、Aが最も多いケース。)

ローカル環境の場合は、上記ケースを標準入力から入力し、期待通りの出力が返ってくるか確かめてみましょう。

TechFUL上でデバッグする方法

しかし、TechFUL上ではサンプルに書かれているケースしか実行できず、自分の試してみたいケースを実行することができません。 TechFUL上ではどのようにデバッグをしたらよいでしょうか。それがこの記事のメインテーマです。

TechFUL上でデバッグしたい場合は、以下の流れで行うことができます。

  1. プログラムを「入力受け取りコード」「問題解答コード」「出力コード」に分割し、「問題解答コード」のみ切り出す。
  2. 「問題解答コード」を関数化する。引数は入力値とし、戻り値は出力値とする。
  3. 関数化した「問題解答コード」をチェックする関数を実装する。チェック関数は入力と想定出力を渡し、関数化した「問題解答コード」の戻り値が想定出力ではない場合はRutimeErrorで強制終了させる。
  4. 試してみたいケースをチェック関数の引数に直打ちする。

文章を読んだだけではよく分からないと思うので、実際にやってみましょう。

実際の問題でデバッグ

先程の問題の解答コードとして、以下のコードを提出してみます。 (今回はc++で書きましたが、他のコードでもデバッグ流れは同じです。) このコードには3つのバグが隠れています。

#include <iostream>
#include <string>
using namespace std;
 
int main(){
    // 1.変数a,bの宣言
    int a;
    int b;
 
    // 2.入力を受け取る
    cin >> a;
    cin >> b;
 
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 < b){
        ans = "Yes";
    }else{
        ans = "No";
    }
 
    // 5.答えを出力
    cout << ans << endl;
 
    return 0;
}

この問題のサンプルケースは以下の2ケースのみで、上記プログラムでは両方「PASSED」になってしまいます。

提出したところ「WA」が複数出ましたので、TechFUL上でデバッグしてみたいと思います。

まず、1番の「コードの分離」を行いましょう。このプログラムを分離すると以下のようになります。

  • 入力受け取りコード:1,2
  • 問題解答コード:3,4
  • 出力コード:5

では次に2番の「問題解答コードの関数化」を行いましょう。

今回は入力がaとbで、出力は"Yes", "No"なので、aとbを引数にして、戻り値がstring型の関数として定義しましょう。 関数化すると以下のようになりました。

#include <iostream>
#include <string>
using namespace std;
 
string func(int a, int b){
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 < b){
        ans = "Yes";
    }else{
        ans = "No";
    }

    return ans;
}
 
int main(){
    // 1.変数a,bの宣言
    int a;
    int b;

    // 2.入力を受け取る
    cin >> a;
    cin >> b;

    // 5.答えを出力
    cout << func(a,b) << endl;

    return 0;
}

次に3番に移りましょう。 こちらのコードをデバッグするためのチェック関数を作成します。 チェック関数は入出力を引数として作成しますので、今回の問題ではa, b, [string型]の3引数の関数として定義しましょう。 [string型]の部分はansと命名したいと思います。

c++では、assert()というマクロを使うことで、「関数化した「問題解答コード」の戻り値が想定出力ではない場合はRutimeErrorで強制終了させる。」の部分を実装することができます。 assert()の引数にFalseを渡すとRuntimeErrorで強制終了します。

こんな感じで書くことができます。

#include <iostream>
#include <string>
#include <cassert>
using namespace std;
 
string func(int a, int b){
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 < b){
        ans = "Yes";
    }else{
        ans = "No";
    }

    return ans;
}
 
void check(int a, int b, string ans){
    assert(func(a, b) == ans);
    return;
}

19行目以降のcheck()関数が追加したコードです。 こちらの関数を用いて、4の「試してみたいケースをチェック関数の引数に直打ちする」を実装してみましょう。

#include <iostream>
#include <string>
#include <cassert>
using namespace std;
 
string func(int a, int b){
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 < b){
        ans = "Yes";
    }else{
        ans = "No";
    }

    return ans;
}
 
void check(int a, int b, string ans){
    assert(func(a, b) == ans);
    return;
}
 
int main(){
    check(1, 1, "No");
    return 0;
}

25行目のような形で、main関数をcheck()関数のみにすれば、「テストする」を押したときの結果で以下のようにチェックできます。 - 結果がWrongAnswer -> そのケースはOK - 結果がRuntimeError -> そのケースはNG

実際に先ほど洗い出した以下のケースでやってみましょう。

  • A=1, B=1のケース(最小ケース)
  • A=1000000000, B=1000000000000000000(最大ケース)
  • A=1, B=99 (Noになる境目のケース)
  • A=1, B=100 (Yesになる境目のケース)
  • A=1000000000, B=2000000000 (Noになるケースのうち、Aが最も多いケース)
check(1, 1, "No");  //WrongAnswerになるのでOK
check(1000000000, 1000000000000000000, "Yes");  //RuntimeErrorになるのでNG
check(1, 99, "No");  //WrongAnswerになるのでOK
check(1, 100, "Yes");  //RuntimeErrorになるのでNG
check(1000000000, 2000000000, "No"); // RuntimeErrorになるのでNG

このように、check()関数でいろいろなケースを試してみるとRuntimeErrorになるケースを見つけることができます。 後はプログラムを読んでバグを見つけ出して、修正しましょう。

ちなみに今回のプログラムのバグは以下の通りです。

  • 2行目のRE -> bがint型なので入力サイズの最大値を格納できない。
  • 4行目のRE -> 条件分岐にイコールを付け忘れている。
  • 5行目のRE -> 条件分岐中にあるa*100がint型ではオーバーフローする。

(備考:TechFULで使用しているコンパイラのint型は32bitです。)

正しく修正したプログラムは以下になります。

#include <iostream>
#include <string>
#include <cassert>
using namespace std;

string func(long long a, long long b){
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 <= b){
        ans = "Yes";
    }else{
        ans = "No";
    }

    return ans;
}

void check(long long a, long long b, string ans){
    assert(func(a, b) == ans);
    return;
}

こうすれば「テストする」の結果が全パターンWAになりました! 注意点として、型が間違っているときはcheck()関数の方も修正を忘れないようにしましょう。

後はこの関数を元のプログラムに戻して提出してみましょう。

#include <iostream>
#include <string>
using namespace std;

string func(long long a, long long b){
    // 3.答えを格納する変数(ansと命名)を宣言
    string ans;
    
    // 4.問題文の条件に合わせて分離
    if(a*100 <= b){
        ans = "Yes";
    }else{
        ans = "No";
    }

    return ans;
}

int main(){
    // 1.変数a,bの宣言
    long long a;
    long long b;

    // 2.入力を受け取る
    cin >> a;
    cin >> b;

    // 5.答えを出力
    cout << func(a,b) << endl;

    return 0;
}

無事正解できました!

まとめ

TechFUL上でデバッグする場合はassert()などの関数を用いて間違いのケースを特定してみましょう。 c++以外にもassert()と同等の機能が提供されていると思いますので、自分の言語の機能を調べてみてください。

今回は主にデバッグの手順について書きましたが、ケースを特定した後のバグの見つけ方や、どうしても自分では間違えるケースを特定することができないときの対処方法などもブログで紹介できればと思います。

この記事が皆さんのお役に立ち、1問でも多くの問題を正解できれば幸いです。 これからもTechFUL問題をよろしくお願いします!