Stormのテストコード実行

テストの実行

Stormのビルドはmaven-clojure-pluginを利用して行われるようになったが、以下のように特定のユニットテストだけを実行することができない様子。

  $ mvn test -Dtest=TestHoge  

REPLを開いてclojure.test/run-testを実行すると、特定のテストモジュールだけを実行することができる。

  $ cd storm-core
  $ mvn compile
  $ mvn clojure:repl
  user=> (use 'clojure.test)
  user=> (require 'backtype.storm.supervisor-test)
  user=> (run-tests 'backtype.storm.supervisor-test)

また、deftestで定義された関数を直接呼び出せば、特定のテストケースだけを実行することもできる。

  user=> (use 'backtype.storm.supervisor-test :reload-all)
  user=> (launches-assignment)

ソースコードを修正した後、useを:reload-all付きで呼び出せば、その場で反映させることができるため、print文デバッグをするときにも便利。

時間の制御

backtype.storm.utile.Timeで以下のようなロジックが定義され、Stormのソースコード中でThread.sleepやSystem.currentTimeMillisの代わりに利用されている。simulatingの値がtrueの場合には、advanceTimeを呼び出さないと時間が進まない状態になるが、これをテストコードの中でタイミングを制御したり、短時間で時間が経過した状態を模擬するために利用している。

    public static void sleep(long ms) throws InterruptedException {
        sleepUntil(currentTimeMillis()+ms);
    }

    public static long currentTimeMillis() {
        if(simulating.get()) {
            return simulatedCurrTimeMs.get();
        } else {
            return System.currentTimeMillis();
        }
    }

    public static int currentTimeSecs() {
        return (int) (currentTimeMillis() / 1000);
    }

    public static void advanceTime(long ms) {
        if(!simulating.get()) throw new IllegalStateException("Cannot simulate time unless in simulation mode");
        simulatedCurrTimeMs.set(simulatedCurrTimeMs.get() + ms);
    }

simulatingがtrueの状態で処理を実行するために、with-simulated-timeというマクロが利用されている。

  (defmacro with-simulated-time
    [& body]
    `(do
       (start-simulating-time!)
       (let [ret# (do ~@body)]
         (stop-simulating-time!)
         ret#)))

以下はテストコードの中で使われる場合の例。with-simulated-time-local-clusterの内側ではsimulatingがtrueで、advance-cluster-timeを呼び出すと、時間が進む。

  (deftest heartbeats-to-nimbus
    (with-simulated-time-local-cluster [cluster :supervisors 0
      :daemon-conf {SUPERVISOR-WORKER-START-TIMEOUT-SECS 15
                    SUPERVISOR-HEARTBEAT-FREQUENCY-SECS 3}]
      (letlocals
        (bind sup1 (add-supervisor cluster :id "sup" :ports [5 6 7]))
        (advance-cluster-time cluster 4)
        ...

ロジックの置き換え

with-var-rootsというマクロを利用して、一時的に特定の関数を置き換えることができる。

  (defmacro with-var-roots
    [bindings & body]
    (let [settings (partition 2 bindings)
          tmpvars (repeatedly (count settings) (partial gensym "old"))
  	vars (map first settings)
          savevals (vec (mapcat (fn [t v] [t v]) tmpvars vars))
          setters (for [[v s] settings] `(set-var-root ~v ~s))
          restorers (map (fn [v s] `(set-var-root ~v ~s)) vars tmpvars)]
      `(let ~savevals
         ~@setters
         (try
           ~@body
           (finally
             ~@restorers)))))

以下は利用例。テストコードの中で、common/storm-task-infoとnimbus/compute-new-topology->executor->node+portを置き換え、nimbusのタスク割り当てロジックをバイパスして、引数として指定された値がそのまま割り当て内容としてセットされるようにしている。

  (defn submit-mocked-assignment
    [nimbus storm-name conf topology task->component executor->node+port]
    (with-var-roots [common/storm-task-info (fn [& ignored] task->component)
                     nimbus/compute-new-topology->executor->node+port (mocked-compute-new-topology->executor->node+port
                                                                        storm-name
                                                                        executor->node+port)]
                    (submit-local-topology nimbus storm-name conf topology)))