2010年2月21日日曜日

自作commandクラスをロードする step(2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
setup.pyから相対パスでcommandクラスを実装したファイルをロードできるようにします。
def get_command_class (self, command):
        """Return the class that implements the Distutils command named by
        'command'.  First we check the 'cmdclass' dictionary; if the
        command is mentioned there, we fetch the class object from the
        dictionary and return it.  Otherwise we load the command module
        ("distutils.command." + command) and fetch the command class from
        the module.  The loaded class is also stored in 'cmdclass'
        to speed future calls to 'get_command_class()'.

        Raises DistutilsModuleError if the expected module could not be
        found, or if that module does not define the expected class.
        """
        klass = self.cmdclass.get(command)
        if klass:
            return klass

        for pkgname in self.get_command_packages():
            module_name = "%s.%s" % (pkgname, command)
            klass_name = command

            try:
                __import__ (module_name)
                module = sys.modules[module_name]
            except ImportError:
                continue

            try:
                klass = getattr(module, klass_name)
            except AttributeError:
                raise DistutilsModuleError, \
                      "invalid command '%s' (no class '%s' in module '%s')" \
                      % (command, klass_name, module_name)

            self.cmdclass[command] = klass
            return klass

        raise DistutilsModuleError("invalid command '%s'" % command)
cmdclassは辞書で、setupの引数で渡すことができます。
from distutils.cmd import Command
class build_js(Command):
  def initialize_options(self):
    pass
  def  finalize_options(self):
    pass
  def run(self):
    print 'build_js is here!'

setup(
  ...
  cmdclass={'build_js':build_js},
  ...
)
前のエントリに加えてこのようにしてやると、次の実行結果を得られます。
[nori@shinano]~/Desktop/study/JavaScript/closure-proj% python setup.py build
running build
build:has_js
Distribution:has_js
running build_js
build_js is here!

build_js.pyとしてまとめるとこのようになる。
#!/usr/bin/env python
# -*- coding: us-ascii -*-
# vim: syntax=python
#
# Copyright 2010 Noriyuki Hosaka bgnori@gmail.com
#


import calcdeps 


def jscompile(inputs, search_paths, output, compiler_jar, compiler_flags):
  import logging
  import os
  import os.path
  cwd = os.getcwd()
  output = os.path.join(cwd, output)
  inputs = [os.path.join(cwd, f) for f in inputs]

  logging.basicConfig(format='calcdeps.py: %(message)s', level=logging.INFO)
  logging.info('Scanning files...')
  logging.info('cwd: %s'%(os.getcwd(),))
  logging.info('search_paths: %s'%(search_paths,))
  search_paths = calcdeps.ExpandDirectories(search_paths)
  #logging.info('expanded paths: %s'%(search_paths,))
  out = open(output, 'w')
  logging.info('Finding Closure dependencies...')
  deps = calcdeps.CalculateDependencies(search_paths, inputs)

  calcdeps.Compile(compiler_jar, deps, out, compiler_flags)
  return output

the_args = {}

from distutils.cmd import Command
class build_js(Command):
  def initialize_options(self):
    pass
  def  finalize_options(self):
    print dir(self)
  def run(self):
    print the_args
    jscompile(**the_args)
def CompiledJS(**attr):
  the_args.update(attr)
  return [True] #place holder

def blackmagic(Distribution, build):
  def xxx(self):
    print 'Distribution:has_js'
    return self.js and len(self.js) > 0
  Distribution.has_js = xxx

  def yyy(self):
    print 'build:has_js'
    return self.distribution.has_js() 
  build.has_js = yyy
  build.sub_commands.append( ('build_js', build.has_js),)

  orig = Distribution.__init__
  def myinit(self, attrs):
    self.js = None
    orig(self, attrs)
  Distribution.__init__ = myinit

setup.pyでこのように呼んであげる。
from build_js import build_js
from build_js import blackmagic
from build_js import CompiledJS

from distutils.dist import Distribution
from distutils.command.build import build

blackmagic(Distribution, build)
setup()の引数。
js=CompiledJS(
    inputs=['src/notepad.js'],
    search_paths=['/home/nori/lib/closure/'],
    output='static/notepad.c.js',
    compiler_jar='/home/nori/bin/closure/compiler.jar',
    compiler_flags=['ADVANCED_OPTIMIZATION'],
  ),
bdist_rpmするとコンパイルして生成したjsファイルがパッケージに含まれない問題があるので、明日はそれに対処したいと思います。

setup.py bdist_rpmでjsをコンパイルする (step1)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
前のエントリで、distutilsの挙動がわかったので本筋に戻る。

まず、build_js.pyを作る。
print 'build_js is here!'
まだ呼び出しが行われることを確認するだけなので、これだけにしておく。

で、setup.pyをいじる。
from distutils.dist import Distribution
def xxx(self):
        print 'Distribution:has_js'
        return self.js and len(self.js) > 0
Distribution.has_js = xxx

from distutils.command.build import build
def yyy(self):
        print 'build:has_js'
        return self.distribution.has_js() 
build.has_js = yyy
build.sub_commands.append( ('build_js', build.has_js),)

from distutils.dist import Distribution
orig = Distribution.__init__
def myinit(self, attrs):
  self.js = None
  orig(self, attrs)

Distribution.__init__ = myinit

def CompiledJS(*args, **attr):
  return ['hogehoge']

setup(
  ...
  js=CompiledJS('/var/www/static/notepad.c.js',
                ['./src/notepad.js'],
                ['/home/nori/lib/closure/'],
                '/home/nori/bin/closure/compiler.jar',
                ['ADVANCED_OPTIMIZATIONS'],
               ),
   ...
)

すると実行されて
[nori@shinano]~/Desktop/study/JavaScript/closure-proj% python setup.py build
running build
build:has_js
Distribution:has_js
running build_js
error: invalid command 'build_js'
となる。あと一息。build_js.pyがloadされるように細工する必要がある。
クラスが変更されるのでbdistからbuildが呼び出された場合でも実行される。
[nori@shinano]~/Desktop/study/JavaScript/closure-proj% python setup.py bdist
running bdist
running bdist_dumb
running build
build:has_js
Distribution:has_js
running build_js
error: invalid command 'build_js'

2010年2月20日土曜日

distutils.buildを理解する。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
大筋でどのようにできているかというと、setupは、distributionを表現するdistがあって、実際の生成プロセスを記述するcommandとsubcommandからなっている。次に関係ないところをすっ飛ばして引用してみた。
  • core.py
    def setup (**attrs):
                dist.run_commands()
    
  • dist.py
    def run_commands (self):
            for cmd in self.commands:
                self.run_command(cmd)
    
  • cmd.py
    def run_command (self, command):
            """Run some other command: uses the 'run_command()' method of
            Distribution, which creates and finalizes the command object if
            necessary and then invokes its 'run()' method.
            """
            self.distribution.run_command(command)
    
  • dist.py
    def run_command (self, command):
            """Do whatever it takes to run a command (including nothing at all,
            if the command has already been run).  Specifically: if we have
            already created and run the command named by 'command', return
            silently without doing anything.  If the command named by 'command'
            doesn't even have a command object yet, create one.  Then invoke
            'run()' on that command object (or an existing one).
            """
            # Already been here, done that? then return silently.
            if self.have_run.get(command):
                return
    
            log.info("running %s", command)
            cmd_obj = self.get_command_obj(command)
            cmd_obj.ensure_finalized()
            cmd_obj.run()
            self.have_run[command] = 1
    
  • cmd.py コメントにあるとおり、サブクラスではメソッドrunを実装している。
    # Subclasses must define:
        #   initialize_options()
        #     provide default values for all options; may be customized by
        #     setup script, by options from config file(s), or by command-line
        #     options
        #   finalize_options()
        #     decide on the final values for all options; this is called
        #     after all possible intervention from the outside world
        #     (command-line, option file, etc.) has been processed
        #   run()
        #     run the command: do whatever it is we're here to do,
        #     controlled by the command's various option values
    
  • install.py
    def run (self):
            # Obviously have to build before we can install
            if not self.skip_build:
                self.run_command('build')
    
            # Run all sub-commands (at least those that need to be run)
            for cmd_name in self.get_sub_commands():
                self.run_command(cmd_name)
    
  • cmd.py (get_sub_commands)
    def get_sub_commands (self):
            commands = []
            for (cmd_name, method) in self.sub_commands:
                if method is None or method(self):
                    commands.append(cmd_name)
            return commands
    
  • build.py sub_commandはclass変数として定義されている。
    sub_commands = [('build_py',      has_pure_modules),
                        ('build_clib',    has_c_libraries),
                        ('build_ext',     has_ext_modules),
                        ('build_scripts', has_scripts),
                       ]
    
  • build_ext.py subcommandの一例として、build_extを追ってみる。
    def run (self):
            ...skip...
            self.compiler = new_compiler(compiler=self.compiler,
                                         verbose=self.verbose,
                                         dry_run=self.dry_run,
                                         force=self.force)
            customize_compiler(self.compiler)
            ...skip...
            # Now actually compile and link everything.
            self.build_extensions()
    
        def build_extensions(self):
            # First, sanity-check the 'extensions' list
            self.check_extensions_list(self.extensions)
    
            for ext in self.extensions:
                self.build_extension(ext)
    
        def build_extension(self, ext):
        ...skip...
            objects = self.compiler.compile(sources,
            ...skip
            self.compiler.link_shared_object(
    
  • ccompiler.py はcコンパイラの抽象。_compileとlinkをsubclassで実装する。
    def compile(self, sources, output_dir=None, macros=None,
        def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
         --> empty
        def link (self,
         --> empty
    
        def spawn (self, cmd):
            spawn (cmd, dry_run=self.dry_run)
         --> from distutils.spawn import spawn
    
  • unixccompiler.py posix系はこのsubclassで実装されている。
    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
            try:
                self.spawn(self.compiler_so + cc_args + [src, '-o', obj] +
                           extra_postargs)
            except DistutilsExecError, msg:
                raise CompileError, msg
    
        def link(self, target_desc, objects,
                    self.spawn(linker + ld_args)
    
  • spawn.py まあ当然ですが、forkに落ちます。
    def _spawn_posix (cmd,
        pid = os.fork()
    
  • build.pyでsub_commandのmappingを定義していたが、そのペアの右側はbuildのメソッドであり、distのmethodに転送される。。なので、このmethodがtrueを返すと、sub_commandが実行される。

    build.py
    def has_scripts (self):
            return self.distribution.has_scripts()
    
    dist.py
    def has_scripts (self):
            return self.scripts and len(self.scripts) > 0
    
  • ではこのscriptsはどこからきているかというとsetupの**attrからきている。
    core.py
    def setup (**attrs):
            _setup_distribution = dist = klass(attrs)
    
    
    dist.py
    class Distribution:
        def __init__ (self, attrs=None):
            if attrs:
                for (key,val) in attrs.items():
                    if hasattr(self.metadata, key):
                        setattr(self.metadata, key, val)
                    elif hasattr(self, key):
                        setattr(self, key, val)
                    else:
                        msg = "Unknown distribution option: %s" % repr(key)
                        if warnings is not None:
                            warnings.warn(msg)
                        else:
                            sys.stderr.write(msg + "\n")
    

2010年2月19日金曜日

twitmateのデータを作ったときのスクリプト

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
かきすてでも読めるレベルになるのがpythonのいいところ。
http://king-soukutu.com/twit/?n=twitmate
http://king-soukutu.com/twit/?n=twitmate2

import re
user = re.compile(r'"http://twitter.com/(?P[A-Za-z0-9_+]+)"')

m = user.search('"http://twitter.com/pothos"')
#print m.group()

found = {}
for i in range(1, 31):
  f = open('following%i.html'%(i))
  lines = f.read()
  ms = user.findall(lines)
  for m in ms:
    found[m] = 'http://twitter.com/%s'%(m,)
  f.close()
for u in found:
  print u

setup.py bdist_rpmでjsをコンパイルする (step0)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
calcdeps.pyから、そのために必要な部分をほじくり出す。optparseで取り出している引数を関数の引数として渡すようにすればよいので、たいしたことはない。問題はこれをsetup.pyのなかでフックするようにするにはどうしたらよいかということ。
import calcdeps 

def jscompile(inputs, search_paths, output, compiler_jar, compiler_flags):
  import logging
  import os
  logging.basicConfig(format='calcdeps.py: %(message)s', level=logging.INFO)
  logging.info('Scanning files...')
  logging.info(os.getcwd())
  logging.info(search_paths)
  search_paths = calcdeps.ExpandDirectories(search_paths)
  out = open(output, 'w')
  logging.info('Finding Closure dependencies...')
  deps = calcdeps.CalculateDependencies(search_paths, inputs)

  calcdeps.Compile(compiler_jar, deps, out, compiler_flags)
  return output

2010年2月18日木曜日

pep8チェック

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
http://pypi.python.org/pypi/pep8/を使ってチェックしてみた。思ったより少なかったなぁ。個人的には変数名とかが単語として辞書にあるかどうかとかもチェックしてほしいが。
[nori@shinano]~/Desktop/work/bglib/bglib% /usr/bin/pep8 bglib 
bglib/doc/__init__.py:7:1: E302 expected 2 blank lines, found 0
bglib/doc/__init__.py:8:3: E111 indentation is not a multiple of four
bglib/doc/__init__.py:10:3: E301 expected 1 blank line, found 0
bglib/doc/__init__.py:26:1: W391 blank line at end of file
bglib/doc/bgwiki.py:21:1: W291 trailing whitespace
bglib/doc/bgwiki.py:34:44: E261 at least two spaces before inline comment
bglib/doc/bgwiki.py:39:3: E303 too many blank lines (2)
bglib/doc/bgwiki.py:46:80: E501 line too long (113 characters)
bglib/doc/bgwiki.py:46:35: E201 whitespace after '('
bglib/doc/bgwiki.py:46:42: E225 missing whitespace around operator
bglib/doc/bgwiki.py:213:13: E222 multiple spaces after operator
bglib/doc/bgwiki.py:310:51: E231 missing whitespace after ':'
bglib/doc/bgwiki_test.py:72:19: E202 whitespace before ')'
bglib/doc/fuzzing_test.py:16:25: W602 deprecated form of raising exception
bglib/doc/html.py:46:14: E221 multiple spaces before operator
bglib/encoding/dbbyte.py:69:27: E203 whitespace before ','
bglib/encoding/gnubgid.py:41:14: E211 whitespace before '('
bglib/encoding/gnubgid.py:158:19: E251 no spaces around keyword / parameter equals
bglib/model/move_test.py:16:18: E701 multiple statements on one line (colon)
bglib/record/gnubg.py:30:13: W601 .has_key() is deprecated, use 'in'

2010年2月17日水曜日

google closure調査中のメモ(コンパイラの制限事項編)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
まとまった情報が欲しい方はごめんなさい。
ほんとに走り書きです。

オプティマイザの制限

全般的な制限:
1)Ecmascript 262 revision 3.しか理解しません。クロスブラウザなものを作ろうとしているのだから、当然の帰結。
2)コメントが消えてなくなります。JSの最適化とは転送量の削減なので、そういうことです。

SIMPLE_OPTIMIZATIONSに関して
- withを取り扱えない。
- evalもだめ。
- a[""]のたぐいもだめ。

ADVANCED_OPTIMIZATIONSにかんして
関数が消えたり、定数値を計算してしまったり。

スコープ外で定義されている変数を使うこと(コンパイラがスコープの外を見ることができないのでだめ)。
明示的なエキスポートしないで内部の名前を外部で使うこと(コンパイラがリネームしてしまうから)
a.hogeとa["hoge"]を混在させる(後者はリネームの対象にならないため)

外で使う関数なら必ずexportする。でないとコンパイラが消してしまう。このへんはCでgccでも-O2とかにするとなるんじゃなかったっけ?

名前の展開(object property flattening)
{}をネストすると遅くなるので、それへの対策として名前を展開する。
var foo = {};
   foo.bar = function (a) { alert(a) };
   foo.bar("hello");
を次のように変換する。
var foo$bar = function (a) { alert(a) };
   foo$bar("hello");
よってこのコードはthisへの参照が破壊される危険がある。のでthisを使わなければならない、つまりメソッド内(正確にはprototype property)とnew関数のなかでやる必要がある。

To prevent property flattening from breaking your references to this, only use this within constructors and prototype methods. The meaning of this is unambiguous when you call a constructor with the new keyword, or within a function that is a property of a prototype.


function Foo() {
  this.bar = function(a){alert(a)};
  return this;
}

参照破壊系のバグが増えるので、*コンパイル後*のコードを使ってunittestを走らせるとよいかもしれません。

2010年2月13日土曜日

QUnut + JSDeferred, テストのためにnextに手を入れる。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
nextの中に手を突っ込んで、callbackの前後でstart/stopすることを目的に書き換えたのだが、なにかうまくいって無くて、testの結果が記録されない。
なにを見落としているのだろう?
var AJAXTIMEOUT = 100;
var surpress_stop  = false;
(function(){
  Deferred.prototype._fire = function (okng, value) {
    var next = "ok";
    start();
    debug('+++');
    try {
      value = this.callback[okng].call(this, value);
    } catch (e) {
      next  = "ng";
      value = e;
      if (Deferred.onerror) Deferred.onerror(e);
    }
    if (!surpress_stop){
      debug('---');
      stop(AJAXTIMEOUT);
    }
    if (value instanceof Deferred) {
      value._next = this._next;
    } else {
      if (this._next) this._next._fire(next, value);
    }
    return this;
  }
})();


2/16追記... startの中でsetTimeoutしているので、nextにフックするべきではない。start/stop pairはtestの中で1つだけにすべき。13msの遅延が何を引き起こすか予想不能。非同期問い合わせが返ってくる前にqueueからテストケースを抜いて実行されると不味い。
        start: function() {
                // A slight delay, to avoid any current callbacks
                if ( window.setTimeout ) {
                        window.setTimeout(function() {
                                if ( config.timeout ) {
                                        clearTimeout(config.timeout);
                                }

                                config.blocking = false;
                                process();
                        }, 13);
                } else {
                        config.blocking = false;
                        process();
                }
        },

2010年2月9日火曜日

JSDeferredを誤解。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
当たり前だけど、「chainのnextとそうでないnextは違う」ということです。debugを挿入して追ってみました。

prototypeのnext
  next  : function (fun) { 
    debug('Deferred.prototype.next');
    return this._post("ok", fun) 
  },

ブラウザ差異を吸収するためのモノのようなので、defaultのみをチェックする。
Deferred.next_default = function (fun) {
  debug('Deferred.next_default');
        var d = new Deferred();
        var id = setTimeout(function () { d.call() }, 0);
        d.canceller = function () { clearTimeout(id) };
        if (fun) d.callback.ok = fun;
        return d;
};

Deferred.next = Deferred.next_faster_way_readystatechange ||
                Deferred.next_faster_way_Image ||
                Deferred.next_default;

実行したテストコード
test('very simple tests for using jsdeferred with qunit.', function(){
  var x = 0;
  Deferred.define();
  next(function(){
    x = 2;
  })
  .next(function(){
    x = 1;
  })
  .next(function(){
    equals(x, 1);
    start();
  });
  stop(100);
});
これも当たり前だけど、グローバルに挿入されているinstanceのものが1回呼ばれた後、prototypeのほうが2回呼ばれます。設計意図を理解せねば。それからDeferred.prototype.fnとDeferred.fnの違いもわからないとだめだなぁ。var d = Deferred; d.fn()とDeferred.fn()は意味が違う。

2010年2月8日月曜日

まださまよっている。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
x=2が行われた後、next()が積まれてx=1が実行され、そしてequals(x, 1)にいく。
test('bad use (3) of jsdeferred with qunit.', function(){
  var x = 0;
  Deferred.define();
  next(function(){
    x = 1;
  });
  equals(x, 0);
  x = 2;
  next(function(){
    equals(x, 1);
    start();
  });
  stop(100);
});

しかし、これはあまり自明じゃない。consoleにはpiyo hogeの順番で表示される。つまりstartが呼ばれてからx=1の代入が行われる。まったく好ましくない挙動。timerを消す必要があるかもしれない。
test('bad use (2) of jsdeferred with qunit.', function(){
  var x = 0;
  Deferred.define();
  next(function(){
    equals(x, 2);
    return next(function(){
      ok(true);
      x = 1;
      debug('hoge');
    });
  });
  equals(x, 0);
  x = 2;
  next(function(){
    equals(x, 2);
    debug('piyo');
    start();
  });
  stop(100);
});

QUnut + JSDeferred

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
  • Why it works?
  • Need to obtain Deferred instance for testing part from the target.

test('bad use (1) of jsdeferred with qunit.', function(){
  var x = 0;
  Deferred.define();
  next(function(){
    next(function(){ x = 1;
    });
  })
  .next(function(){
    equals(x, 0);
    start();
  });
  stop(100);
});

test('bad use (2) of jsdeferred with qunit.', function(){
  var x = 0;
  Deferred.define();
  next(function(){
    return next(function(){
      x = 1;
    });
  });
  next(function(){
    equals(x, 0);
    start();
  });
  stop(100);
});

test('right way: some thing bit complex using jsdeferred with qunit.', function(){
  var x = 0;
  var d = Deferred.define();

  d.next(function(){
    debug('first next ');
    return next(function(){
      debug('nested next');
      x = 1;
    });
  })
  .next(function(){
    debug('equals next ');
    equals(x, 1);
    start();
  });
  stop(100);
});

2010年2月6日土曜日

QUnitのstartとstop

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
QUnitはjQueryのunittest frame work. setTimeoutで13ってなんだよ!
クロスブラウザのJSのunittest frameworkで非同期のテストをサポートしているモノはほかに何があるのだろう?知っている方がいたら教えてください。

from jQueryのテスティングフレームワーク QUnit

start( ) : テストを再開します。

stop( ) : テストを中断します。Ajaxで非同期通信等のテストを行うとき、非同期処理前にstop()でテストを止め、非同期処理終了時のコールバック関数内で start()を実行して、テストを再開します。

とのこと。これだけではどういう機構なのかさっぱりわからないのでソースを見てみる。
  start: function() {
    // A slight delay, to avoid any current callbacks
    if ( window.setTimeout ) {
      window.setTimeout(function() {
        if ( config.timeout ) {
          clearTimeout(config.timeout);
        }

        config.blocking = false;
        process();
      }, 13);
    } else {
      config.blocking = false;
      process();
    }
  },

  stop: function(timeout) {
    config.blocking = true;

    if ( timeout && window.setTimeout ) {
      config.timeout = window.setTimeout(function() {
        QUnit.ok( false, "Test timed out" );
        QUnit.start();
      }, timeout);
    }
  },
  • stopは一時中断で、あまり長く中断しているとテスト失敗にする。通常は非同期な動作を開始する前に呼ぶ。つまり非同期動作が長いということはその非同期動作の制御が戻ってきていないということになる。
  • startの13は謎。processを呼ぶとテストが走るのだと思う。
さらにprocessを追ってみる。
function process() {
  while ( config.queue.length && !config.blocking ) {
    config.queue.shift()();
  }
}
  1. キューから関数を抜いてそれを実行する。
  2. config.blockingをたててテストを中断し、フラグをクリアしてprocessを呼べば再開できる。キューから抜かれてしまったモノは中断できないはず、というかブロッキングですね。
古いMacOSの協調的マルチプロセッシングを思い出します。pre-emptされないのでプログラムは自発的に制御を明け渡さないといけない。キューに詰められるitemの粒度が大きいと気持ち悪いですね。

JSSpecのなか。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
コアっぽいコードを発見。そのなかでも特に注目すべきは次の点だろう。
  • setTimeout 0している。
  • targetの中で各test caseを実行しているのだろう。
    result = self.target();
       self.onSuccess(self, result);
    
JSSpec.Executor.prototype.run = function() {
  var self = this;
  var target = this.target;
  var onSuccess = this.onSuccess;
  var onException = this.onException;

  window.setTimeout(
    function() {
      var result;
      if(JSSpec.Browser.Trident) {
        window._curExecutor = self;

        result = self.target();
        self.onSuccess(self, result);
      } else {
        try {
          result = self.target();
          self.onSuccess(self, result);
        } catch(ex) {
          if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line};

          if(JSSpec._secondPass)  {
            ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
            delete JSSpec._secondPass;
            delete JSSpec._assertionFailure;

            ex.type = "failure";
            self.onException(self, ex);
          } else if(JSSpec._assertionFailure) {
            JSSpec._secondPass = true;
            self.run();
          } else {
            self.onException(self, ex);
          }
        }
      }
    },
    0
  );
};
もっともhighレベルで呼ばれるrunはこれ。Runnerがspecからcaseを読み込んでexecuterとして追加し、その後実行している。
JSSpec.Runner.prototype.run = function() {
        JSSpec.log.onRunnerStart();
        var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true);
        for(var i = 0; i < this.specs.length; i++) {
                executor.addExecutor(this.specs[i].getExecutor());
        }
        executor.run();
};
addExecuterの中身。Executerインスタンスが作るツリーをトラバースしながらcaseを実行していく。 Composite Patternを使っているのでCompositeExecuter。Unittestの実装では普通な実装。 呼び出す順序はここで決まってしまう。
JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) {
        var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1];
        if(last) {
                last.next = executor;
        }
        
        executor.parent = this;
        executor.onSuccessBackup = executor.onSuccess;
        executor.onSuccess = function(result) {
                this.onSuccessBackup(result);
                if(this.next) {
                        this.next.run();
                } else {
                        this.parent.onSuccess();
                }
        };
        executor.onExceptionBackup = executor.onException;
        executor.onException = function(executor, ex) {
                this.onExceptionBackup(executor, ex);

                if(this.parent.continueOnException) {
                        if(this.next) {
                                this.next.run();
                        } else {
                                this.parent.onSuccess();
                        }
                } else {
                        this.parent.onException(executor, ex);
                }
        };

        this.queue.push(executor);
};

大筋で理解はしたつもりだが、Deferredと混ぜるのはどうすれば良いのやら。

JSSpec + jsDeferredどうしよう?(動機編)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
当たり前なんですが、このテストは意図に反して成功します。なぜならnextに与えている関数はたぶん1つも実行されないためです。最後nextで呼び出されるはずのshould_beは実行されません。。通る前にテストが終わってしまうのです。

マルチスレッドでいうところの「待ち合わせ」が必要なのです。Deferredの中のキューをのぞき見て、非同期のリクエストがすべて完了したことを見計らってテストから抜けてもらわないといけないのですが、ユーザ側でなんと化しようとするとyieldとかが無いとたぶん無理でそれはJSの標準にはありません。もしくはJSSpecになんらかのpatchを当てて待ち合わせるように書き直す必要があるのです。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>jsboard.jy JSSpec results</title>
<link rel="stylesheet" type="text/css" href="../JSSpec/JSSpec.css" />
<script type="text/javascript" src="../JSSpec/diff_match_patch.js"></script>
<script type="text/javascript" src="../JSSpec/JSSpec.js"></script>

<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">// <![CDATA[
google.load("jquery", "1.4.0");
// ]]></script>
<script type="text/javascript" src="../jsdeferred/jsdeferred.jquery.js"></script>
<script type="text/javascript">// <![CDATA[
describe('async', {
        'before': function() {
                target = {};
        },
  'テスト' : function (){
  var x = 0;
  var d = Deferred.define();
  d.next(function (){
    x =1;
  })
  .wait(1000)
  .next(function(){
    value_of(x).should_be(1);
    value_of(x).should_be(0);
  });
  }
});



// ]]></script>
</head>
<body><div style="display:none;"><p>A</p><p>B</p></div></body>
</html>

2010年2月2日火曜日

LRU at BPStudy with JSSpec.

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
まあまあ手になじむ。あとは非同期なモノをどうかけるのかをチェックするべし。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>LRU JSSpec results</title>
<link rel="stylesheet" type="text/css" href="../src/JSSpec.css" />
<script type="text/javascript" src="../src/diff_match_patch.js"></script>
<script type="text/javascript" src="../src/JSSpec.js"></script>
<script type="text/javascript">// <![CDATA[

function debug(){
  if (window['console']){
    console.log.apply(null, arguments);
  }
};

function LRU(maxSize){
  return {
    '_store' : [],
    '_maxSize': maxSize,
    'maxSize': function(){
      var self = this;
      return self._maxSize;
    },
    'put': function(key, value){
      var self = this;
      var p;
      var n;
      var v;
      p = self._peek(key);
      n = p[0];
      v = p[1];
      if (n >=0){
        self._store = self._store.slice(0, n).concat(self._store.slice(n+1));
        debug('self._store', self._store);
        self._store.push([key, value]);
        return;
      }else{
        self._touch(key, value);
      };
    },
    '_peek': function(key){
      var self = this;
      var n;
      var k;
      var v;
      for (n in self._store){
        k = self._store[n][0];
        v = self._store[n][1];
        if (k==key){
          return [n, v];
        };
      };
      return [-1, null];
    },
    'get': function(key){
      var self = this;
      var n;
      var k;
      var v;
      p = self._peek(key);
      n = p[0];
      v = p[1];
      if (v != null){
        self._touch(key);
      };
      return v;
    },
    _touch: function(key, new_value){
      var self = this;
      var p;
      var n;
      var v;
      p = self._peek(key);
      n = p[0];
      v = p[1];
      if (n >=0){
        // found so pop and append key-value pair with new value.
        self._store = self._store.slice(0, n).concat(self._store.slice(n+1));
        self._store.push([key, v]);
        return;
      };

      // not found in _store, 
      if (typeof(new_value) != 'undefined'){
        self._store.push([key, new_value]);
        if (self._store.length > maxSize){
          self._store = self._store.slice(1);
        };
        return;
      }
    }
  };
};


describe('LRU', {
        'before': function() {
                target = LRU(2);
        },
        'should 現在の最大サイズを返す。': function() {
                value_of(target.maxSize()).should_be(2);
        },
        'should keyに対応する値がなければnullを返す。': function() {
                value_of(target._peek('foo')[1]).should_be(null);
        },
        'should keyに対応する値を内部状態を変えることなく返す。': function() {
                target.put('foo', 42);
                value_of(target._peek('foo')[1]).should_be(42);
        },
        'should keyの値が同じなら上書きされる。': function() {
                target.put('foo', 0);
                target.put('foo', 42);
                value_of(target._peek('foo')[1]).should_be(42);
        },
        'should sizeよりたくさん入れると始めに入れたやつが消える。': function() {
                target.put('foo', 42);
                target.put('bar', 43);
                target.put('buzz', 44);
                value_of(target._peek('foo')[1]).should_be(null);
                value_of(target._peek('bar')[1]).should_be(43);
                value_of(target._peek('buzz')[1]).should_be(44);
        },
        'should getすると居残る。': function() {
                target.put('foo', 42);
                target.put('bar', 43);
                value_of(target.get('foo')).should_be(42);
                target.put('buzz', 44);
                value_of(target._peek('foo')[1]).should_be(42);
                value_of(target._peek('bar')[1]).should_be(null);
                value_of(target._peek('buzz')[1]).should_be(44);
        },

})

// ]]></script>
</head>
<body><div style="display:none;"><p>A</p><p>B</p></div></body>
</html>

JSSpecを始めました。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
BPStudyでRSpecが気になったのと、JavaScriptのコードをテストしなくなったので、JSSpecを始めて見た。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>LRU JSSpec results</title>
<link rel="stylesheet" type="text/css" href="../src/JSSpec.css" />
<script type="text/javascript" src="../src/diff_match_patch.js"></script>
<script type="text/javascript" src="../src/JSSpec.js"></script>
<script type="text/javascript">// <![CDATA[

function LRU(size){
  return {
    'size': function(){
      var self = this;
      return 9;
    }
  };
};


describe('LRU', {
        'before': function() {
                target = LRU(2);
        },
        'should 現在のサイズを返す。': function() {
                value_of(target.size()).should_be(2);
        }
})

// ]]></script>
</head>
<body><div style="display:none;"><p>A</p><p>B</p></div></body>
</html>