ログ日記

作業ログと日記とメモ

Google Closure Library を使ったJavaScriptをブラウザ無しでテストする

試行錯誤して長くかかったのでメモ。


まず、Closure Library は最初の頃はJSのファイルを羅列したhtmlを読み込んでテストする方式だったと思う。
JsUnit がその形だったかな?
CIとかが手軽に出来なかった頃はそれでも問題なかった。今ではテストのハードルがぐっと上がるので、ブラウザを自分で開いて確認するのはよろしくない。

jsunit test report not showing tests · Issue #869 · google/closure-library · GitHub

昔から使っていた人も他のツールに移っていっている模様。



今では(というか数年前?)ブラウザをコマンドで立ち上げてブラウザ機能を使いつつ、コンソールに結果を表示するという方法が取られるようだ。
blockly/run_mocha_tests_in_browser.js at master · google/blockly · GitHub
Googleのプロジェクトでサンプルを見れる。
このプロジェクトだとES5で書かれているから問題ないようだけど、ES6のmoduleを使うと、file:// で読み込めない問題がある。別途Webサーバーを立ち上げないといけない。
E2Eテストが遅いからJavaScriptだけでテストしたいのに、こんなに色々な準備が必要なら本末転倒である。見た目は綺麗で良い感じなんだけど。


かと言ってシンプルにmocha だけ使っても、documentが無いのでクライアントのテストができない。
Mocha - the fun, simple, flexible JavaScript test framework



MVC的にうまく切り分けられているならModelだけテストできそうだけれども、Closure LibraryはControl系コンポーネント以外はViewもゴチャ混ぜになっている。
そういうわけで色々試した結果、mocha と jsdom を使うことにした。
mochaはテストフレームワーク、jsdomはdomを提供してくれる。
(mocha-jsdomというパッケージもあるが、更新されていないようだった)

npm install --save-dev mocha
npm install --save-dev jsdom


mochaディレクトリを作り、初期化用ファイルを置く。

mkdir mocha
vi mocha/init.js
vi mocha/accordion_test.js


init.js

require('../lib/closure-library/closure/goog/bootstrap/nodejs.js');

// Polyfill for encoding which isn't present globally in jsdom
var util = require('util');
global.TextEncoder = util.TextEncoder;
global.TextDecoder = util.TextDecoder;

goog.nodeGlobalRequire('build/mocha_deps.js');

jsdomに無い関数を割り当てたりしながら、deps.jsも読み込む。
nodeを使いつつモジュール以外のファイルを読み込む場合は goog.nodeGlobalRequire を使うようだ。


mocha_deps.js は依存関係が書かれたファイル。
pathの扱いがややこしく、普段Closure Libraryで使っているものをそのまま使えなかった。base.jsとnodejs.jsはpathが一段深くなるので、deps.jsを生成するときのroot_with_prefixの指定を一段深くする必要があった。
これは別のファイルを生成することに。


mocha_js.sh

#!/bin/bash

set -eux

cd `dirname $0`/../

./lib/closure-library/closure/bin/build/depswriter.py \
    --root_with_prefix="js ../../../../js" \
    > ./build/mocha_deps.js


実際のテストのファイルはこんな感じ。


accordion_test.js

var JSDOM = require('jsdom').JSDOM;
var assert = require('assert');
var FakeTimers = require("@sinonjs/fake-timers");

goog.require('app.anim.Accordion');
var Accordion = goog.module.get('app.anim.Accordion');

goog.require('goog.style');
var clock;

describe('app.anim.Accordion', function() {
  before(function(){
    var dom = new JSDOM(
      '<html><body><form name="form">'
        + '<input type="checkbox" name="check">'
        + '<div id="area" style="display:none">test</div>'
        + '</form></body></html>');
    global.window = dom.window;
    global.document = window.document;

    this.check_ = document.forms.form.elements.check;
    this.area_ = document.getElementById('area');

    clock = FakeTimers.install();

    this.nowOrig_ = goog.now;
    goog.now = (function() {
      return +new Date();
    });
  });
  after(function(){
    this.check_ = null;
    this.area_ = null;

    clock.uninstall();
    goog.now = this.nowOrig_;
  });
  describe('#decorate()', function() {

    it('can decorate Accordion', function() {
      var target = new Accordion(this.check_, 100);
      target.decorate(this.area_);
      assert.equal(target.isInDocument(), true);
    });

    it('should change shown and running animation', function() {
      var target = new Accordion(this.check_, 100);
      target.decorate(this.area_);

      assert.equal(goog.style.isElementShown(this.area_), false, 'element is not shown before starred');
      this.check_.dispatchEvent(new window.Event('change'));

      assert(target.anim_, "exists anim object");
      assert.equal(target.anim_.isPlaying(), true, 'anim is started');
      assert.equal(goog.style.isElementShown(this.area_), true, "element is shown just after clicked");

      clock.tick(20); // start delay

      assert.equal(target.anim_.isPlaying(), true, 'anim is still started');
      assert.equal(goog.style.isElementShown(this.area_), true, "element is shown");

      clock.tick(80);

      assert.equal(target.anim_.isPlaying(), false, 'anim is stopped');
      assert.equal(goog.style.isElementShown(this.area_), true, "element is shown");
    });
  });
});

チェックボックスがオンされるとアコーディオンがアニメーションで開く、というコンポーネントのテスト。
ハマりポイントが結構あった。


最初は init.js でjsdomを読み込んでいたけれど、テストごとに別のhtmlを使うと思ってtest.jsに移動した。逆に言うと、htmlが同じなら*test.jsを同じファイルにまとめられる。


ES6を読み込む場合は goog.require と goog.module.get の両方が必要なことに注意する。


Closure Libraryの時刻系処理は全てgoog.nowを使っていて、それは読み込み時に既にnew Date()が割り当てられているので、goog.nowも置き換える必要がある。めっちゃライブラリのソース読んだ。
domの幅は取れなかった。画面が無いもんね。なのでstyleを確認。


イベントはjsdomで作ったwindowのEventをnewすればOK。




最後に、GitLab用のCIファイルを書く。
.gitlab-ci.yml

mocha_test:
  image: registry.gitlab.example.com/app/nodejs-python:12.0
  stage: test
  before_script:
    - npm install
  script:
    - ./bin/mocha_js.sh
    - ./node_modules/mocha/bin/mocha --file mocha/init.js "mocha/**/*test.js"
  except:
    - schedules

nodejs-python は自分でビルドしてGitLabのレジストリに登録した。


Dockerfile

FROM node:12-buster-slim

RUN apt-get -y update && apt-get -y upgrade && apt-get -y install python

バージョンが古いのは、今使っているdebianに合わせるため。普通にbuster-slimからapt install nodejs でも良かったかも。



キャッシュが無い状態で1分半、キャッシュがあれば53秒だった。
なかなか良い感じではないかな。


これでやっとJavaScriptのテストが書けるという最低限の状態にできた。

Debian の /bin と /usr/bin と debootstrap

https://n314.hatenablog.com/entry/2021/10/07/200305 の続き。



/bin/ping と /usr/bin/ping のバイナリが異なることに気付いた。

/bin/ping ならゲストからでもエラーが出ない。
でもPATHの設定で/usr/bin/ping が優先されるのでエラーが出るようだ。
/usr/bin/ping は getcap で /bin/ping = cap_net_raw+ep が付いていない。更新日も2018年で/bin/pingは2021年だった。


エラーが出ない方のゲストは /usr/bin/ping と /bin/ping が同一で、ホストは /usr/bin/ping が存在していなかった。


UsrMerge - Debian Wiki
debootstrap を実行したタイミングによって異なっている?


iputils-ping を削除すると /bin/ping は削除されるが /usr/bin/ping はそのままのようだ。
これと同じように、openssl系のライブラリもどこかに優先されるPATHで古いバージョンが残っている…?

2021-03-28 のiputils-ping の更新で、 /bin/ping, /usr/bin/ping の両方が更新されているパターンと /bin/ping だけ更新されているパターンがあるっぽい。


これは、もうこの状態になったらどうしようもないのかな。エラーが出るサーバーだけ手動で /etc/ca-certificates.conf を変更していくしかない?

2021-10-01 に Let's Encrypt の SSLでエラーが出る模様

systemd-nspawn ゲストの一般ユーザーでネットが繋がらない。
何もしてないのに壊れた。

$ git remote show origin
fatal: unable to access 'https://example.com/app/app.git/': server certificate verification failed. CAfile: none CRLfile: none
$ ping localhost
ping: socket: 許可されていない操作です

$ LANG=C ping localhost
ping: socket: Operation not permitted

検索するとSSL系の設定やsuidの記事が出てくるが、違う感じがする。pingは元々 /usr/ping は u+s ではない。
libcap2-bin 系も違う。別の環境ではgetcapの結果は同じなのに使えたりするので。

ログにも異常は見つけられず。
pingはrootだと問題なさそうなので、権限系っぽいが…。

ホストごと再起動してみるも直らず。
opensslは自動アップグレードがかかっているけど、それだとpingは関係ないよねえ…。
systemdのセキュリティ更新は二ヶ月以上前で、その後は問題なかった。



pingが問題ないマシンもあるので、違いを見てみる。
grep Cap /proc/$$/status の結果や getcap /bin/ping の結果は同じ。

$ grep Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000fdecbfff
CapAmb: 0000000000000000

# getcap /bin/ping
/bin/ping = cap_net_raw+ep

他にも試す。

export GIT_SSL_NO_VERIFY=1

あれ?これでいけた。
もしかして最初からpingは使えなかった?
うーん…。でもopensslの更新も二ヶ月前だった。なんで?




9/27は問題なし、今月になってから問題になったということは…

www.walbrix.co.jp

もしかしてこれ?
wgetはダメだった。curlはいける。
git は rootでもダメだった。

openssl が 1.1 なので完全にスルーしてた。ホストもゲストも同じバージョンなのにゲストだけエラーが出る。何かパッケージが足りないのだろうか?


/usr/share/ca-certificates/ にも違いが無い。
update-ca-certificates をやっても変わらない。

/etc/ssl/certs/ca-certificates.crt もエラーが無いゲストとエラーがあるゲストで同じだった。…うーん。






www.mail-archive.com
stackoverflow.com

最悪ローカルのcrtを何とかすればいいっぽいけど…。普通にパッケージ更新していれば大丈夫だとか、update-ca-certificatesでうまくいくって書いてあるよね。

openssl s_client -CApath /etc/ssl/certs -showcerts -connect lists.debian.org:443 < /dev/null

openssl のコマンドでは成功するけど、wgetが成功しない。分からん…。


community.letsencrypt.org

新しい環境でダメな人も居るみたい。


マジで困った。



/etc/ca-certificates.conf の

mozilla/DST_Root_CA_X3.crt

この行の先頭に ! を入れて、update-ca-certificates をすれば取りあえずはしのげる。
でも、この手作業の対応をそれぞれのサーバーでやる…?まったく同じバージョンで問題ないサーバーもあるのに…?
うーん…すっきりしない。

update-ca-certificates --fresh --verbose

でも変わらず、wgetでエラーが出るサーバーとエラーが出ないサーバーがある。

muttを設定する

ちょっとミーハーな心持ちで、以前挫折したCLIメーラーをインストールしてみる。
一応
GnuPG を使えるメーラーを探す - ログ日記
の続き。


大量のメールを処理できるNeomutt - Solist Work Blog
ここを読んでNeomuttが気になったけど
NeomuttでGmailにOAuth2.0する - Qiita
Debian(buster) の場合はMuttよりNeomuttの方が古いっぽいので。
bullseye なら大丈夫っぽい。


muttをインストールすると

/usr/share/doc/mutt/examples/mutt_oauth2.py

があるので、どこかのbinにコピーしてくる。

python のファイルの上の方に registrations 変数があって接続情報が書かれているので、直接編集。
ENCRYPTION_PIPE にもGPGで使っているメールアドレスを書く。


/usr/share/doc/mutt/examples/mutt_oauth2.py.README を読みつつ
Mutt - ArchWiki
ここも読みながら muttrc を設定する。


先にGoogleAPIでoauthのIDを発行しておく。テスト用でいけた。


とりあえず接続テストは

./bin/mutt_oauth2.py user@example.com.tokens --verbose --authorize

のようにして質問に答えていってから

./bin/mutt_oauth2.py user@example.com.tokens --verbose --test

で確認。


muttrcでのGmailの設定は

set from="user@example.com"
set realname="User Name"
set imap_user="user@example.com"
set folder="imaps://imap.gmail.com/"
set smtp_url="smtps://${imap_user}@smtp.gmail.com:587/"
set imap_authenticators="oauthbearer:xoauth2"
set imap_oauth_refresh_command="/home/user/bin/mutt_oauth2.py /home/user/.mutt/${imap_user}.tokens"
set smtp_authenticators=${imap_authenticators}
set smtp_oauth_refresh_command=${imap_oauth_refresh_command}

このように書いた。

あとは設定を適当なサンプルから持ってきた。

set spoolfile = "+INBOX"
set imap_check_subscribed
set hostname = gmail.com
set mail_check = 120
set timeout = 300
set imap_keepalive = 300
set postponed = "+[GMail]/Drafts"
set record = "+[GMail]/Sent Mail"
set trash = "+[GMail]/Trash"
set header_cache=~/.mutt/cache/headers
set message_cachedir=~/.mutt/cache/bodies
set certificate_file=~/.mutt/certificates
set signature =~/.mutt/signature


Gmailを見れるようにはなった。
でも、使えるかと言われると…うーん。
初めてvimを起動して終了方法すら分からなかったときような感じ。


Claws MailじゃなくてSylpheedを試してみようか。

DebianでVMwareが遅い問題は解決してなかった

https://n314.hatenablog.com/entry/2021/01/28/104312
こっちの設定をやってしばらく使ってるけど、やっぱりVMwareの起動直後はkcompactd0がフルにCPUを使って3分ぐらいほとんど操作できない。

/sys/kernel/mm/transparent_hugepage/defrag の設定は madvise にしていた。これってアプリケーションが必要だからデフラグを要求してるってことだよねえ…。
VMware用にメモリを12GB設定しているから、その分のデフラグは必要だってことなんだろうか。

固まるのは起動時だけで、最初にちょっと待てば後は大丈夫なんだけど。


qiita.com
今度VMware起動前と起動後で断片化をチェックしよう。

PHPStanとPsalmを混ぜた機能が欲しい

PHPStanはテンプレート+callableのネストに対応していた。

<?php

/**
 * @template A
 * @template B
 * @template X
 * @param callable(A,B):X $f
 * @return callable(A):(callable(B):X)
 */
function f2(callable $f)
{
    return function($a) use ($f){
        return function ($b) use ($f, $a){
            return $f($a, $b);
        };
    };
}

/**
 * @template A
 * @template B
 * @template C
 * @template X
 * @param callable(A,B,C):X $f
 * @return callable(A):(callable(B):(callable(C):X))
 */
function f3(callable $f)
{
    return function($a) use ($f){
        return function($b) use ($f, $a){
            return function($c) use ($f, $a, $b){
                return $f($a, $b, $c);
            };
        };
    };
}

function str_repeat_split(string $s, int $time, string $separator):string
{
    $arr = [];
    for ($i = 0; $i < $time; $i++){
        $arr[] = $s;
    }
    return implode($separator, $arr);
}
function test(): string
{
    $f = f3('str_repeat_split');
    $g = $f('abc');
    $h = $g(1);
    // $h(1);
    $h("\n");

    $i = f2('str_repeat');
    //$j = $i('abc')('def');
    $j = $i('abc')(2);

    $k = f2('str_repeat')('abc');
    //$l = $k('def');
    $l = $k(2);

    // $m = f2('str_repeat')('abc')('foo');
    $n = f2('str_repeat')('abc')(2);
    return nl2br($n);
}

コメントを外すと正しいエラーが報告される。

ここで、括弧がちょっとめんどいので

<?php

$n = f2('str_repeat')('abc')(2);
$n = f2('str_repeat', 'abc', 2);

引数と関数呼び出しの区別を無くして、並べて呼び出せるようにしたい。

機能だけなら
github.com
昔作ったんだけど、型チェックが効かない。


PsalmのConditional return typesの機能を使ってfunc_num_argsで戻り値の型を条件分岐させたい。
でもPsalmは

 * @return callable(A):(callable(B):(callable(C):X))

みたいな複雑な型に対応してないっぽい。



github.com
PHPStanにもconditional return typesの機能を付ける話は出ているみたい。

That's what this open feature request asks for, it's on my roadmap to implement this.

issue登録者が間違ってクローズしたものを作者が再オープンしていたので、しばらく待てば実装されるんだろうか。
もうすぐ1年経ってしまうけれども。

haskell-language-server の設定2

https://n314.hatenablog.com/entry/2021/07/24/173834 の続き。

emacs lsp-mode "disconnected"」で検索した https://github.com/emacs-lsp/lsp-mode/issues/905 ここのコメントに M-x lsp してログを見ろと書いていたので、そのようにする。

lsp-haskell stack "Cradle requires ghc but couldn't find it"

という警告が出ていた。
というかそもそもEmacs起動時に

Server lsp-haskell:27768/starting exited with status exit(check corresponding stderr buffer for details). Do you want to restart it? (y or n)

というエラーも出ていた。

検索して出てきた
haskell-language-server+emacsでのハマりどころ2020年夏
このページを見てもよく分からなかったけど、そういえば hie.yaml が必要だったとどこかに書いていた記憶がある。
手動で作ってうまくいかなくて消したんだった。


hie.yamlでもう一度調べる。
(廃止) VS Code と haskell-ide-engine で Haskell 開発環境を構築する
Haskell環境構築2020簡易版 (macOS, Linux向け) - LugendrePublic

stack install implicit-hie
gen-hie > hie.yaml

そして stackプロジェクトのMain.hsを起動。

emacs app/Main.hs &

おおお、いけたじゃん!


あとは修正と設定。

Error running timer ‘lsp--on-idle’: (wrong-type-argument integerp 9.223372036854776e+18)

というエラーが出るのを回避する。
https://github.com/emacs-lsp/lsp-mode/issues/2435

(setq lsp-headerline-breadcrumb-enable nil)
Error while checking syntax automatically: (error "Keyword argument :end-line not one of (:buffer :checker :filename :line :column :message :level :id :group)")

というエラーが出るのでflycheckをバージョンアップ。



型を自動で挿入したいので

(defun lsp-haskell-execute-code-action-add-signature ()
  "Execute code action of add signature.
Add the type signature that GHC infers to the function located below the point."
  (interactive)
  (let ((action (seq-find
                 (lambda (e) (string-prefix-p "add signature" (lsp:code-action-title e)))
                 (lsp-code-actions-at-point))))
    (if action
        (lsp-execute-code-action action)
      (message "I can't find add signature action for this point"))))
https://www.ncaq.net/2021/06/25/18/20/30/

これを何かのキーに割り当てる。

オプションで

{-# OPTIONS_GHC -Wmissing-signatures #-}

を設定してwarningが出るようにしておく必要があるっぽい。


あとは定義ジャンプ。
Emacs で Language Server Protocol を使ってみる :: プログラマになりたい人生 — プログラマになりたいおじさんの日記



右に色々出たりコードに線が出るけど、まだあまり意味が分かっていない。
Fold とか Unfold は何なんだろう…?IDEで関数を閉じたりする?haskell-modeでは使わない?
Use pointとかreduceとかは押したら文字が削られる…。
How do you use (un)folding? · Issue #418 · haskell/haskell-language-server · GitHub
よく分からん…。
他にも、haskellではなくてlsp-modeのreadmeを見た方がいいかも。



7/31 追記:

Main.hs is not part of any project. Select action:

i==>Import project root path/to/project/
I==>Import project by selecting root directory interactively.
d==>Do not ask again for the current project by adding /path/to/project to lsp-session-folders-blacklist
D==>Do not ask again for the current project by selecting ignore path interactively.
n==>Do nothing: ask again when opening other files from the current project.

というメッセージがミニバッファに出る場合は i を押す。
一瞬で消えたりするので、他のキーを触らずに i を押す。M-x lsp でもう一度見れる。

もしかして以前やったときはこのメッセージを見逃していたのかもしれない。


あと、他のファイルに飛べない?
xref-find-definitions で Main.hs から Lib.hs に飛べない。

と思ったけど、Lib.hs と同じ階層にFoo.hsを作ったらそこには飛べる。
stack new したプロジェクトの構成が分かってないのが問題か…。

Haskellのstackによるプロジェクトについて - Qiita
コメントも参考になる。

Stackでやる最速Haskell Hello world! (GHCのインストール付き!) - Qiita
チュートリアル