たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
(この記事はQiitaとのクロスポストです。)
初心者丸出し感がものすごいのですが、標題の通りのことが起こってしまいました。とあるJavaScript製の自作ソフトウェアの文法チェックにeslintやjsonlint-cliを使いたくて、package.json
を置いておいて、npm install
でそれらをインストールできるようにしていたのですが、それが突然以下のようなエラーで止まるようになってしまいました。
$ npm install
npm ERR! invalid options argument
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2021-05-xxTxx_xx_xx_xxxZ-debug.log
他の自作ソフトウェアでも、npm install
しようとすると同じエラーで止まってしまいました。メッセージには、詳細を見たければログを読むように書かれており、そのログファイル(/root/.npm/_logs/2021-05-xxTxx_xx_xx_xxxZ-debug.log
)を見ると、具体的なエラー箇所は以下のような感じでした。
27 verbose stack TypeError: invalid options argument
27 verbose stack at optsArg (/usr/local/lib/node_modules/npm/node_modules/mkdirp/lib/opts-arg.js:13:11)
27 verbose stack at mkdirp (/usr/local/lib/node_modules/npm/node_modules/mkdirp/index.js:11:10)
27 verbose stack at tryCatcher (/usr/local/lib/node_modules/npm/node_modules/bluebird/js/release/util.js:16:23)
27 verbose stack at ret (eval at makeNodePromisifiedEval (/usr/local/lib/node_modules/npm/node_modules/bluebird/js/release/promisify.js:184:12), <anonymous>:13:39)
27 verbose stack at Object.mkdirfix (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/util/fix-owner.js:36:10)
27 verbose stack at makeTmp (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/content/write.js:121:19)
27 verbose stack at write (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/lib/content/write.js:35:19)
27 verbose stack at putData (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/put.js:11:10)
27 verbose stack at Object.x.put (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/cacache/locales/en.js:28:37)
27 verbose stack at WriteStream._flush (/usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/node_modules/make-fetch-happen/cache.js:156:21)
27 verbose stack at WriteStream._write (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:36:35)
27 verbose stack at doWrite (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:428:64)
27 verbose stack at writeOrBuffer (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:417:5)
27 verbose stack at WriteStream.Writable.write (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream/lib/_stream_writable.js:334:11)
27 verbose stack at WriteStream.end (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:45:41)
27 verbose stack at WriteStream.end (/usr/local/lib/node_modules/npm/node_modules/flush-write-stream/index.js:42:47)
依存関係のどこかで、あるライブラリの動作が変更され、それに依存していた別のライブラリが動作しなくなる、という事態は度々発生します。RubyでもNode.js(JavaScript)でも、おそらくはPHPでもPythonでも、パッケージマネージャを使っていると、「色々なライブラリが依存関係で自動的に入ってくるけれども、そのそれぞれの内容はチンプンカンプン。にもかかわらず、依存関係が原因の実行時エラーが発生してしまって問題解決の糸口すら掴めない」という状況が発生しがちでしょう。
僕はJavaScriptを長く書いてきてはいますが、パッケージマネージャそのものには明るくなく、このような状況が発生するとお手上げになりがちで、そういう意味ではまったく初心者レベルと言えます。今回も、エラーが出た瞬間に顔が真っ青になり、操作を繰り返しても状況が変わらないことでさらに血の気を失う、という絶望的な状況でした。
結論から先に言うと、この問題はNode.jsを入れるのに使っていたnのバージョンが古かったせいで発生していました。n自体を最新の物に入れ換えてn 16.1.0
で現行の最新リリースのNode.jsを入れ直した所、この問題は無事に解消され、npm install
が成功するようになりました。
以下は、今回この問題の解決策に辿り着き、原因を把握するまでの過程で行ったことの記録です。今回の問題自体は、誰の環境でも起こるという物ではないので、直接の参考にはならないと思いますが、未知の物と立ち向かいながら調査をして、現状を把握し問題解決を図る際の、暗闇の歩き方の参考にしてもらえれば幸いです。
ES moduleのコードでは「関数名や変数名の誤記」をESLintなどで容易に静的に検出できます。なので、JSで書ける物は片っ端からなんでもES moduleにしたくなるのですが、いわゆる自動テストのコードでは、この性質がかえって邪魔になることがあります。
たとえばMochaのテストケースには、何の前触れもなくdescribe()
やit()
などの関数が登場します。これらが「未定義の関数呼び出し」としてエラーにならないようにするには、テストと実装でESLintのルールを切り替えて警告条件を緩和したり、テスティングフレームワークの関数やオブジェクトを警告の例外に明示したりといった対策が必要になります。
しかし、警告を甘くすればテストだけ静的な検証が甘くなりますし、警告の例外を指定するのもテストの作成やメンテナンスが煩雑になります。テストの書きやすさ・維持しやすさと静的な検証の完全性とを両立しにくいのは、JSで物を作る時に個人的にずっと気になっていた点でした。
というわけで、このような難点がないPure ES modulesなテスティングフレームワークを作ってみました。
Node.js v13以上の環境で、npm install tiny-esm-test-runner
でインストールできます。
元々は、特定プロジェクトでのCI用に簡易的なテストランナーを書いて使い捨てにしていたのですが、それを複数プロジェクトで使い回すうちに、さすがメンテナンスが面倒だと感じるようになってきたため、この度一念発起してきちんと整備したという次第です。
テストはES moduleのファイルとして記述します。以下に例を示します。
FirefoxのアドオンやChromeの拡張機能向けに、名前空間をまたいでDOMに変更を差分適用したい場面で使える、Virtual DOMでないReal DOMで差分適用する、webextensions-lib-dom-updater
という名前のライブラリをつくりました。
クライアント側でタブの情報を取得して、サーバー側でそれをレンダリングする、という場面であれば以下のようになります。
クライアント側(制御担当):
// IDからタブのオブジェクトを得る(WebExtensionsのAPI)
const tab = await browser.tabs.get(tabId);
// プロセスをまたいで、レンダリングして欲しい内容を送る
browser.runtime.sendMessage(
'受信側の識別子',
// ↓テンプレート記法でHTMLのコード片をそのまま生成
`
<span id="tab"
class="${tab.active ? 'active' : ''}">
<span id="throbber"
class="${tab.status}">
<span id="throbber-image"
class="${tab.status}"></span>
</span>
<img id="favicon"
class="${tab.status}"
src="${tab.favIconUrl}">
<span id="label">${tab.title}</span>
</span>
`.trim()
);
サーバー側(画面描画担当):
import { DOMUpdater } = './dom-updater.js';
// 他のプロセスからのメッセージを待ち受ける(WebExtensionsのAPI)
browser.runtime.onMessageExternal(message => {
// 反映先の要素
const before = document.getElementById('container');
// 反映する内容をDocumentFragmentにする
const range = document.createRange();
range.setStart(document.body, 0);
const after = range.createContextualFragment(message);
range.detach();
// DocumentFragmentの内容でbeforeと異なる部分があれば、
// それをbeforeに差分適用する
DOMUpdater.update(before, after); // ←これを作った。
});
Virtual DOMでなく生のReal DOMを更新内容として指定する(※例ではDocumentFragmentを使ってますが、普通のElementでも構いません。)ので、Virtual DOMの独自記法を覚えなくていいです。利点はそれだけです。
既に同じ事をするライブラリが世の中にはあったのかもしれませんが、自分には見つけられませんでした。どなたかご存じでしたら教えてください……
→と書いていたら、morphdomという似た趣旨のライブラリが既にあると教えて頂きました。今回実装したものとの比較を最後に追記しました。
Tree Style TabというFirefox用アドオンで、「他のアドオンから指示して、タブの中に任意のUI要素を追加する」という事をやるために作りました。
見た目を元々のタブに合わせているのでちょっと分かりにくいですが、このスクリーンショットの左側で「Add-ons - Mozilla | MDN」というラベルを伴って表示されている「細いタブっぽい物」が、別のアドオンから指示された通りの内容を、このライブラリによる差分適用で埋め込んだ部分です。
アドオン間での通信ではJSONオブジェクト形式のメッセージしか扱えないため、こういう事をやろうとすると
ということを決める必要があります。
DOMの変更の差分適用といえば既存のVirtual DOM専用のライブラリは既にいくつもあって、
このあたりの記法をそのまま使えばいいといえばいい話です。
が、どれもべつに「スタンダード」というわけではないようなので、どれを選んでも後で文句を言われそうな気がします。宗教戦争がもしあるなら、そこに参戦したくはないですし、ただでさえ「Tree Style Tabが他のアドオン向けに提供する独自のAPI」というめちゃめちゃニッチな場面なので、こんな限定された場面のために新たに(もし普段から使っている物があるなら、それとは別のライブラリ由来の)独自の記法を覚えてもらうのは忍びないです。というか、自分がこれ以上覚えたくありません。
その点、HTMLのソースを文字列で指定してDOMの標準的な機能でNodeやDocumentFragmentにするという事にしておけば、多少冗長ではあるものの、「デジュールスタンダードなんで」と言ってしまえます。技術選択で悩まなくてもよくするためだけの選択というわけです。
前に似たような事をやった気がするけど。
javascript:
var tab = ' ',
min = prompt('Input minimum level of the headings (default=1)');
function tabs(n) {
var ret = [];
for (var i = 0; i < n; i++) ret.push(tab);
return ret.join('');
};
function collectHeadings(minLevel) {
var rawHeadings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
var headings = [];
var heading, node;
for (var i = 0, maxi = rawHeadings.length; i < maxi; i++)
{
node = rawHeadings[i];
heading = {
node: node,
id: node.id,
label: node.textContent,
level: parseInt(node.localName.charAt(1))
};
if (heading.level >= min)
headings.push(heading);
}
return headings;
}
function generateList(headings) {
var list = [],
h,
id,
item,
nest = 0;
for (var i = 0; i < headings.length; i++)
{
h = headings[i];
id = h.id || h.node.parentNode.id || h.node.parentNode.parentNode.id;
item = (id) ? '['+h.label+'](#'+id+')' : h.label ;
if (i > 0) {
if (h.level > headings[i-1].level) {
nest += 1;
} else if (h.level < headings[i-1].level) {
nest -= 1;
}
}
if (nest == 0) {
list.push(' * '+item+'\n');
} else {
list.push(tabs(nest)+'* ' + item + '\n');
}
}
return list.join('');
}
var headings = collectHeadings(Math.max(1, min));
var list = generateList(headings);
if (list) window.open('data:text/plain,'+encodeURIComponent(list));
見出しレベルの関係がちゃんとしてないとうまく動かない。あとsection/headingのネストには対応してない。
I got a mail.
I've started to use jsdeferred 2 days ago and I'm really really happy with it because I consider it's really easy to use, light and it looks very powerfull. Anyway, I have one little doubt I hope you can help me about.
Here's the thing, I would like to deferredize some functions to use in some chains, but I haven't figured out what's the best way to get it, I put you a little example
If I try something basic like this:
function my_deferred(data) { var deferred = new Deferred(); deferred.call(data); return deferred; } function main() { Deferred.define(); my_deferred("foo"). next(function(data) { console.log(data); }); }
The chain is not executed (I don't know why). But if I change to something like this, using setTimeout, it works:
function my_deferred(data) { var deferred = new Deferred(); setTimeout(function() { deferred.call(data); },1); return deferred; } function main() { Deferred.define(); my_deferred("foo"). next(function(data) { console.log(data); }); }
The question is, wouldn't it be possible to get the right behaviour of the chain without using setTimeout? I would like to deferredize some functions without the penalty of using setTimeout.
You should use Deferred.next()
( it is available as a global function
next()
if you use Deferred.define()
) instead of setTimeout()
, like:
function my_deferred(data) {
var deferred = new Deferred();
Deferred.next(function() {
deferred.call(data);
});
return deferred;
}
Deferred.next()
works just like setTimeout()
, but in most cases it works
faster than setTimeout()
.
deferred.call()
just calls the next job which is registered as a
callback function given to its .next()
method. So, you have to call
deferred.call()
after you register the next job.
However, In your case, any "next job" has not been registered yet when
you call deferred.call()
. As the result, the "next job" registered after
deferred.call()
was called won't be called by anyone.
function my_deferred(data) {
// (3) Now, we are in this function.
var deferred=new Deferred();
deferred.call(data); // (4) deferred.call() is called, then
// the next job ( registered via
// deferred.next() ) is also called.
// However, it has not been registered yet.
// As the result, nothing happens.
return deferred; // (5) We return the deferred object and
// exit from this function.
}
function main() {
// (1) Starting position of this event loop.
Deferred.define();
// (2) The function is called.
my_deferred("foo").
next(function(data) {
// We never come here!!
console.log(data);
}); // (6) You register the next job to the returned deferred
// object, via its next() method. However, because
// deferred.call() was already called, the job will
// never be called by anyone.
// (7) End of this event loop.
}
Instead,
function my_deferred(data) {
// (3) Now, we are in this function.
var deferred=new Deferred();
Deferred.next() {
// (8) Now, the next event loop is started.
deferred.call(data); // (9) deferred.call() is called, then
// the registered next job is also called.
}); // (4) We just reserve the task for the next event loop.
return deferred; // (5) We return the deferred object and
// exit from this function.
}
function main() {
// (1) Starting position of this event loop.
Deferred.define();
// (2) The function is called.
my_deferred("foo").
next(function(data) {
// (10) We are here!
console.log(data);
}); // (6) You register the next job to the returned object.
// (7) End of this event loop.
}
As above, you must register the next job to the deferred
object before its call()
is called.
ちょよんごさんによると、このメールの送り主の人(ここでは省いたけど、WebGL用のスクリプトを書いてるそうです)は、JSDeferredを使ってるとおぼしき開発者に手当たり次第質問を投げてるらしい。そんなこととはつゆ知らず「僕作者じゃないんだけどなあ」と呑気にちょよんごさんに転送してしまって、余計なストレスをかけてしまったようなので罪滅ぼしと思って真面目に返信してみた。伝わってるかどうかは分かんないけど。
これはJSDeferredの結構典型的なハマリ所だと思う。XMLHttpRequestとかNode.jsの非同期なメソッドみたいに、コールバック関数が必ず次以降のイベントループで呼ばれる物に対してであれば、このメールの送り主の人が書いてるような
var Deferred = require('jsdeferred').Deferred;
var fs = require('fs');
function statDeferred(filePath) {
var deferred = new Deferred();
fs.stat(filePath, function(error, stats) {
if (error)
deferred.fail(error);
else
deferred.call(stats);
});
return deferred;
}
statDeferred('/tmp/foobar')
.next(function(stats) {
if (stats.isDirectory()) {
...
}
});
こういう流れの書き方で何も問題ない。
問題は、やらせたい処理が同期的であった場合で、
function statDeferred(filePath) {
var deferred = new Deferred();
try {
var stats = fs.statSync(filePath);
deferred.call(stats);
} catch(error) {
deferred.fail(error);
}
return deferred;
}
これでは動かないのですよね。
何故かというと、 deferred.call()
は「その deferred
の next()
に渡された関数、という形で あらかじめ 登録されていた『次のジョブ』を実行する」物だから。
この例だと statDeferred()
が deferred
を return
する前に deferred.call()
を呼んでしまっているから、その後で次のジョブを登録しても、登録した「次のジョブ」を呼ぶ人が誰もいないわけです。
function statDeferred(filePath) {
// (2) 関数の中に入った。
var deferred = new Deferred();
try {
var stats = fs.statSync(filePath); // (3) 処理を実行した。
deferred.call(stats); // (4) 登録済みの「次のジョブ」を実行しよう……
// とするんだけど、まだ何も登録されてないので、
// 当然何も起こらない。
} catch(error) {
deferred.fail(error);
}
return deferred; // (5) 関数を抜けた。
}
// (1) 今のイベントループでのスタート地点。
statDeferred('/tmp/foobar')
.next(function(stats) {
// 「次のジョブ」の登録後に deferred.call() が呼ばれないので、ここには到達しない。
if (stats.isDirectory()) {
...
}
}); // (6) 戻り値の next() を呼んで、「次のジョブ」を登録した(今更)。
// (7) 今のイベントループの終わり。
だから、 setTimeout()
なり Deferred.next()
なりを使って、 deferred.call()
を呼ぶ処理を「次のジョブを登録する処理」よりも後(=次のイベントループ)に実行する必要がある。そうすれば、
function statDeferred(filePath) {
// (2) 関数の中に入った。
var deferred = new Deferred();
Deferred.next(function() {
// (7) 次のイベントループで、予約されたこの処理が始まる。
try {
var stats = fs.statSync(filePath);
deferred.call(stats); // (8) 登録済みの「次のジョブ」を連鎖的に呼ぶ。
} catch(error) {
deferred.fail(error);
}
}); // (3) 次のイベントループで実行するように予約。
return deferred; // (4) 関数を抜けた。
}
// (1) 今のイベントループでのスタート地点。
statDeferred('/tmp/foobar')
.next(function(stats) {
// (9) 「次のジョブ」として、この処理が始まる。
if (stats.isDirectory()) {
...
}
}); // (5) 戻り値の next() を呼んで、次のジョブを登録した。
// (6) 今のイベントループの終わり。
……という順番で処理が進むから、期待通りに動くようになる。
どの処理が一体何をしているのか、どの関数が何を返しているのか、自分が呼んでいるメソッドは誰のメソッドなのか、といった事をちゃんと理解しておくと、この話がすっと頭に入ってくると思うんだけど、ただの構文として覚えてしまっていると、メールの送り主の人みたいに「何で動かないの……」って詰まってしまうんだと思う。
という風に偉そうな事を言ってるけど、僕自身も最初は多分ただの構文として覚えてた方で、ただ、「同期処理の時は setTimeout()
なり Deferred.next()
なりを使って deferred.call()
を呼ぶ」という所までを「構文」として暗記していたから、結果的にはちゃんと動いてくれてたという状態だったんだと思う。その後若手IT勉強会の中でコードリーディングをやってだんだん仕組みが分かってきて、「ああなるほど、こういうことだったんだ」と理解できるようになった。
Firefox Hacks Rebootedでもいっぱい書いたけど、JSDeferredの本質は、「あらゆる処理について、その次にやらせたい仕事を next()
のコールバック関数という形で後から登録できるようにする」ものだ、と僕は認識してる。「後から登録する」という都合上、この性質は非同期処理(関数を抜けた時点でまだ処理が始まっていないという種類の処理)と非常に相性がいい。けれども、非同期処理じゃないと使えないわけではない。ただ、「後から登録する」というのは、普通に順番通りに進む同期的な処理の中では行えないイレギュラーな事だから、 setTimeout()
や Deferred.next()
を使わざるを得なくて、それで初見の人に分かりにくくなってしまう。そこがもどかしい所だ。
2011年11月30日付けのNightly 11.0a1でツリー型タブが動かないっていう報告を受けたので調べてみたら、なんかJavaScriptベースでアニメーション効果を実装する時に使えるFirefox 4から(だったっけ?)の新しい機能に基づいたアニメーション管理のための仕組みが期待した通りに動かなくなってて、あれこれ試してるうちにどうもmozRequestAnimationFrame()の使い方のうちDOMイベントベースでやる方のが動かなくなっててコールバック関数を使う方法でならうごくっぽいという事が分かったので、ツリー型タブ0.12.2011120101からはそのように変更した。
えむけいさんが教えて下さった所によると、これはBug 704171 – Remove the no-argument form of requestAnimationFrameでの変更による物で、要するに「DOMイベントベースの方法は正式な仕様になりそうにないし誰も使ってないし、もう廃止してよくね?」ってことで、Geckoでしか使えなかったDOMイベントベースの方法は廃止されて、WebKit等でも利用できるのと同じ形式のAPIに統一されたんだそうだ。まさにその当日にMDNのドキュメントを調べてて、その廃止された方の使い方が解説されていて、MDNにも書いてある使い方なのにおっかしーなーと首をひねってたんだけど、単にドキュメントの更新が間に合っていなかっただけだった。
現実的には妥当な決定だと思うけど、なんか釈然としない。例外のメッセージで「仕様が変わった、古い使い方はもう使えない」とかそういう情報を出してくれれば、もうちょっとすんなり原因に気がつけただろうに……
JSDeferredは非同期処理の制御に特化しててサイズも小さくて素敵な軽量ライブラリだ!と僕は思ってるんだけど、世の中を見回してみると「軽量ライブラリ」って言われてる物は3KB未満とかそういうのが結構あるようだし、jQueryも1.5.1のminified版は28KBって書いてあるし、そうなるとjsdeferred.jsのコメント付き版が0.3.4で19KBというのは「軽量」と呼ぶにはひょっとしてちょっとでかいのかな……という気がしてきた。
なので、JS MinifierとかPackerとかそういう風なやつでどれくらい小さくなるのか実験してみた。
/packer/はそのままだと構文エラーで動かなくなってしまった。}Deferred.
って部分が7箇所あってこれがエラーになってるので、全部 };Deferred.
に置換(Base62圧縮後だと }4.
を };4.
に置換)したら一応エラーは出なくなった。/packer/の構文解析が貧弱なせいっぽいので、JSDeferredの側で問題になる所にあらかじめセミコロンを入れておけば、この問題は無くなるっぽい気がする。……と書いたからか、ちょよんごさんが対応してくれた。ありがとうございます!
あと、closure-compiler を通すと 2668bytes でした (jsdeferred のレポジトリで rake すると URL が出るようになってます)
というアドバイスも頂いたので早速Closure Compilerにかけてみた所確かに小さくなったのですが、シンボル類まで全部失われちゃってこれ単体だと他のスクリプトと組み合わせられないのが残念ですね。(Closure Compilerは他のスクリプトも全部合わせて一緒にコンパイルして使うのが前提ってことなんだろう)
96桁目のセルに所定の値が入ったCSVを作る、みたいな事をやらなきゃいけなくなったんだけど、GnumericにしろExcelにしろカラム名が数字じゃなくてA, B, C, ... Z, AA, AB, ... ZZ, AAA, ...というヘンテコ表記になってるからどこのセルに値を入れればいいかわかんないよウワァァァァン!!!! となったのでJavaScriptで解決した。
var input = prompt('input number or column name');
if (!input) return;
var symbols = 'abcdefghijklmnopqrstuvwxyz';
var result;
if (/^[0-9]+$/.test(input)) {
input = parseInt(input);
result = [];
while (input > 0) {
result.unshift(symbols.charAt((input - 1) % symbols.length));
input = Math.floor((input - 1) / symbols.length);
}
result = result.join('').toUpperCase();
}
else {
result = 0;
input = input.toLowerCase().split('').reverse();
for (var i = 0, maxi = input.length; i < maxi; i++) {
result += (symbols.indexOf(input[i]) + 1) * Math.pow(symbols.length, i);
}
}
alert(result);
カラム名→カラム番号の計算方法は分かったけど、頭が悪い僕にはカラム番号→カラム名の計算式が分からなかったので、検索して出てきたエクセルの1000列目は ALL - つまみ食うのアルゴリズムを丸パクリした。阿呆ですんません……
なんでそこで詰まったかっていうと、単純な26進数の変換ではうまくいかなかったからなんですよね。A==0、B==1という感じで26進数として扱うと、26==Zで27==BAになるんだけど、実際は繰り上がった後はBAじゃなくてAAにならなきゃいけない。そこの所を考慮した計算を多分簡単なループで実現できるとは思ったんだけど、頭が働かなくて無理だった。
最近、JsDoc Toolkitの導入を考えてる。
ツリー型タブのAPI紹介等ではXPIDLっぽい書き方にしてみてるけど、オレオレ表記なので分かってもらえない可能性があるという心配はずっとしている。
ところで、Firefox自体のソースを見ていると以下のような書き方をしているのをよく見かける。
(略)
/**
* Given a starting docshell and a URI to look up, find the docshell the URI
* is loaded in.
* @param aDocument
* A document to find instead of using just a URI - this is more specific.
* @param aDocShell
* The doc shell to start at
* @param aSoughtURI
* The URI that we're looking for
* @returns The doc shell that the sought URI is loaded in. Can be in
* subframes.
*/
function findChildShell(aDocument, aDocShell, aSoughtURI) {
(略)
これはJavaで標準的に使われているJavadocという「ソースコードの中に埋め込まれたコメントを自動的に収集してHTML形式でドキュメントを生成する」仕組みに基づいたもので、同じ形式でコメントを埋め込めるよう、Javadocの仕様に準拠した実装が言語ごとに存在しているようだ。JavaScriptならJsDoc Toolkit、C言語ならGTK-Docが一般的らしい。あとJavaScriptに関してはGoogle Closure ToolsのClosure Compilerも対応しているらしい
事実上の標準としてみんなが見慣れた形式なのであるならば、これに合わせて書くのがいいだろう。と思ったので、とりあえずライブラリとして切り出して公開しているコードにJavadoc形式で使い方の解説を埋め込んでみることにした。
@example
に例を書く時に、例の中に<
とか>
とかのHTML的にまずい文字が含まれていると、出力されるHTMLがぶっ壊れてしまう。JsDoc Toolkit自体のソースを見てみた所(JsDoc Toolkitはそれ自体がJavaScriptで書かれている。Java上で動作するJavaScript実行環境のRhinoの上で動作している。)、ファイルの拡張子でフィルタリングを行っているらしいということが分かった。あと、テンプレートのファイルの方を編集すれば、例に埋め込んだコードのせいでHTMLがぶっ壊れてしまう問題は回避できるようだった。
そういうわけで当面の所はこんな変更を加えて使ってみることにした。以下はjsdoc_toolkit-2.3.2.zipに対する差分です。
diff -ur jsdoc-toolkit-orig/app/lib/JSDOC/JsDoc.js jsdoc-toolkit/app/lib/JSDOC/JsDoc.js
--- jsdoc-toolkit-orig/app/lib/JSDOC/JsDoc.js 2009-01-24 18:42:04.000000000 +0900
+++ jsdoc-toolkit/app/lib/JSDOC/JsDoc.js 2010-08-20 10:17:17.602464000 +0900
@@ -69,7 +69,8 @@
JSDOC.JsDoc._getSrcFiles = function() {
JSDOC.JsDoc.srcFiles = [];
- var ext = ["js"];
+ var ext = ["js", "jsm"];
+ var ignorePattern = /\.test\.js$/i;
if (JSDOC.opt.x) {
ext = JSDOC.opt.x.split(",").map(function($) {return $.toLowerCase()});
}
@@ -89,7 +90,7 @@
}
}
- return (ext.indexOf(thisExt) > -1); // we're only interested in files with certain extensions
+ return (ext.indexOf(thisExt) > -1) && !ignorePattern.test($); // we're only interested in files with certain extensions
}
)
);
diff -ur jsdoc-toolkit-orig/app/run.js jsdoc-toolkit/app/run.js
--- jsdoc-toolkit-orig/app/run.js 2009-01-08 06:32:58.000000000 +0900
+++ jsdoc-toolkit/app/run.js 2010-08-16 17:28:28.673092400 +0900
@@ -337,7 +337,7 @@
if (!path) return;
for (var lib = IO.ls(SYS.pwd+path), i = 0; i < lib.length; i++)
- if (/\.js$/i.test(lib[i])) load(lib[i]);
+ if (/\.jsm?$/i.test(lib[i])) load(lib[i]);
}
}
Only in jsdoc-toolkit: jsdoc.bat
diff -ur jsdoc-toolkit-orig/templates/jsdoc/class.tmpl jsdoc-toolkit/templates/jsdoc/class.tmpl
--- jsdoc-toolkit-orig/templates/jsdoc/class.tmpl 2009-09-03 06:37:31.000000000 +0900
+++ jsdoc-toolkit/templates/jsdoc/class.tmpl 2010-08-18 15:10:02.542253900 +0900
@@ -300,7 +300,10 @@
<if test="data.example.length">
<for each="example" in="data.example">
- <pre class="code">{+example+}</pre>
+ <pre class="code">{+String(example)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')+}</pre>
</for>
</if>
@@ -399,7 +402,10 @@
<if test="member.example.length">
<for each="example" in="member.example">
- <pre class="code">{+example+}</pre>
+ <pre class="code">{+String(example)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')+}</pre>
</for>
</if>
@@ -466,7 +472,10 @@
<if test="member.example.length">
<for each="example" in="member.example">
- <pre class="code">{+example+}</pre>
+ <pre class="code">{+String(example)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')+}</pre>
</for>
</if>
@@ -565,7 +574,10 @@
<if test="member.example.length">
<for each="example" in="member.example">
- <pre class="code">{+example+}</pre>
+ <pre class="code">{+String(example)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')+}</pre>
</for>
</if>
モック(Mock)とスタブ(Stub)の違いがよく分かってなかったんだけど、何が違うのか、そしてモックはどう使う物なのかということを、すとうさんに教えてもらって今更理解した。あとで会社のブログに書くつもりだけど、メモとして要点だけまとめておく。
自分は今まで、とりあえずユニットテストに注力していて、ある意味脅迫観念的な勢いで、ブラックボックス度合いを高くする事を心がけてた。今まではそれでだいたい問題なかった。でも最近になって、ブラックボックステストにしようとすると無理があるというケースにぶち当たるようになった。1つの機能の中でコロコロと遷移する内部状態を、どうにかして検証したいというようなケースが出てきた。
それですとうさんに相談したら、そういう時はモックを使えばいいと言われた。でも、話を聞く限りだとモックというのはテスト対象の実装の中の処理の流れを追う物のようなので、それじゃブラックボックステストにならないじゃないかと思った。それをそのまま言ったら、確かにテストはできるだけブラックボックステストになってた方がいいけど、機能テストやインテグレーションテストのような粒度の大きな単位のテストでは、処理の中で起こる様々な出来事や副作用を色々モニタリングして、すべての処理が期待通りに動いているかどうかを検証しないといけないから、必然的にホワイトボックステストにならざるを得ないと言われた。
それを聞いて、目の覚めるような思いをした。そうか、ブラックボックステストとホワイトボックステストの使い分けはそこが基準になるのか、と。今まで自分がホワイトボックステストを書かずに済んでいたのは、状態の遷移を伴うような機能を作る必要がなかったからだったんだ、テストをどうも書きにくいなあと思っていた機能は本当はホワイトボックステストにしたほうがいい物だったんだ、と。
そんなわけでUxUにモックの機能を実装した。
「JavaScript Mock」で検索するとJSMockとjqmockが上位に出てきたので、最初はそれらを参考にするように(メジャーな実装があるんだったらそれをそのまま取り入れるなりAPIを合わせるようにするのが望ましい)と言われたんだけど、ドットで繋げるメソッドチェインの記法がガンガン出てきて頭パンクした。
もう少し下の方までスクロールするとMockObject.jsというのが出てきて、こっちはファイル全体で3KBに満たない小さなライブラリなので、まずはここから始めることにした。何せコードが短いから、読むのもそんなに苦にはならない。モックの概念を言葉で説明されてもさっぱりだったけど、一通りの処理の流れを見たら、「モックというのは一体何をやらなきゃいけないのか」「どういう振る舞いが期待されているのか」ということがよく分かった。
MockObject.jsと同等の機能を一通り実装した後でもう一度JSMockの方を見たら、なるほどこれはこういう意味だったのかというのがやっと分かった。サンプルコードを見ても、モックという物の意味をそもそもよく知らない時点では、どこからどこまでがJSMockの部分なのかさっぱり分からなかったんだよね。ということで、MockObject.jsに加えてJSMock互換のAPIも付け加えてみた。jqunitの方は……もう別言語だからシラネ。
あと有名なのはJsMockito? これも頑張ったらできるかなあ、というかMITライセンスだしそのままぶち込んだ方が早いか……