20200331のdockerに関する記事は10件です。

Docker

趣味でプログラミングを勉強しています。
軽量に動作して高速に環境を構築できる「Docker」(ドッカー)を今回記事にしてみました。

Docker-logo-icon.jpg

Dockerとは?

コンテナ技術を使ったアプリケーションの実行、運用をするためのプラットフォーム

コンテナとは?

ホスト上に論理的な区画(コンテナ)を作り、アプリケーションを動作させるのに必要なライブラリやアプリケーションを1つにまとめ、あたかも個別のサーバのように使うことができるようにしたもの
→ホストOSのリソースを論理的に分離して複数コンテナで共有して使うので、間接的にかかるコストが少なく軽量で高速に動作するのが特徴

コンテナの歴史

FreeBSD jail
https://ja.wikipedia.org/wiki/FreeBSD_jail
Solaris Containers
https://www.oracle.com/technetwork/jp/server-storage/solaris10/containers-169727-ja.html

Dockerの機能

  1. Dockerイメージ(コンテナの元)を作る機能
    Dockerイメージ アプリケーションの実行に必要なファイル群が格納されたディレクトリ

  2. Dockerイメージを共有する機能
     ・Dockerレジストリ
       イメージを持つリポジトリ(イメージの集まり)を預かるサービス
     ・DockerHub https://hub.docker.com/
       Docker、コンポーネントで動くリソースを集めた場所

  3. Dockerコンテナを動かす機能

Dockerのエディションは2つ

コミュニティ版(CE; Community Edition)
エンタープライズ版(EE; Enterprise Edition)

Dockerが動く仕組み

・コンテナという独立環境を作り、コンテナを区画化してアプリケーションの実行環境を作る
・Linuxカーネルのnamespaceという機能を使いコンテナを仮想的に隔離する
・Dockerコンテナと外部ネットワークが通信を行うときは、NAPTの機能を使って通信する

Linux namespaces
https://en.wikipedia.org/wiki/Linux_namespaces

NAPT
1つのIPアドレスを複数のコンピューターで共有する技術で、IPアドレスとポート番号を変換する機能

Dockerのインストールと動作確認

環境

Windows 10 Pro 64bit メモリ 16G
Docker for Windows v 19.03.5

1. Docker Desktop for Windowsをインストール
https://hub.docker.com/editions/community/docker-ce-desktop-windows/

2. コマンド実行
docker container run amazonlinux:latest //bin/echo 'Introduction to Docker'

説明

docker container run コンテナを作成/実行
amazonlinux:latest もとになるDockerイメージ
//bin/echo 'Introduction to Docker' コンテナ内で実行するコマンド

2020-03-30_16h32_01.png

1回目はダウンロードするので時間がかかるが、
2回目以降はダウンロードされたDockerイメージをもとにDockerコンテナを起動するので高速に動く

Dockerコマンド

Dockerバージョン
docker version

実行環境の詳細設定
docker system info

ディスクの利用状況
docker system df

Dockerを使ってWebサーバを構築

コマンド実行
docker container run --name webserver -d -p 8000:80 httpd

説明

--name webserver -d 「webserver」という名前のコンテナをバックグラウンドで起動
-p コンテナのポートをホスト側に公開
8000:80 ホスト8000番ポートとコンテナ80番ポートをマッピング

バックグラウンド
相対的に低い優先度で動作し、入力がごく少ないか全くなく、最小限の出力を行うプロセス

2020-03-30_17h46_37.png

httpdサーバの状況確認
docker container ps

コンテナ詳細
docker container stats webserver

httpd停止
docker stop webserver

httpd起動
docker start webserver

今回はDockerのさわりを記事にさせていただきましたので、
次回、「Docker Compose」を使った複数コンテナの環境構築を記事にしたいと思います。

ダウンロード.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerでGemfile(gem追加)更新したら後、データベースが消えた、gemが反映しない

サーバーを再起動してますか????

#dockerではない通常開発の場合

dockerなし
control c でサーバー終了
rails s

これでgem反映します。

ということはdockerもサーバーを再起動しなければいけない。

dockerあり
$ docker-compose restart

これで反映されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2020年はCOBOL元年なので1時間でHello Worldに挑戦

はじめに

2020年は色々あってCOBOLが世界を制します。
1時間でチュートリアルしてみました。

実行環境

Dockerで動くものを用意しました。
リポジトリ

COBOLのランタイムはこちらからダウンロードしたものをあらかじめgit管理しています。

実行コマンド

$ git clone https://github.com/glassmonkey/hello-cobol
$ cd hello-cobol
$ docker-compose up

出力

$ hello world

ソースコード

QiitaにCOBOLのシンタックスなくてわろた。

プログラムちょい替え(4)COBOLを40年ぶりにうごかしてみた:dockerでcobol, docker(81)からソースコードお借りしました。

000010 IDENTIFICATION                   DIVISION.
000020 PROGRAM-ID.                      SAMPLE-01.
000030*
000040 ENVIRONMENT                      DIVISION.
000050*
000060 DATA                             DIVISION.
000070*
000080 PROCEDURE                        DIVISION.
000090 MAIN.
000100     DISPLAY "Hello world!"  UPON CONSOLE.
000110     STOP RUN.

プログラムの構成

COBOLプラグラム入門様を元に参考にしました。
COBOLは部と段落から構成させるプログラム言語とのこと。部は段落から構成されており、それぞれ部には必要な段落を記載するとのこと。

見出し部(IDENTIFICATION DIVISION)

プログラムのメタ情報を記述するところ。今回は内部的にSample-01と名前をつけている。
作成者などもここに書く模様。

環境部(ENVIRONMENT DIVISION)

実行マシン名(SOURCE-COMPUTER)や環境変数の受け渡し情報やプログラムから読み書きするファイルの名前や種類を書くところ。今回は空。

データ部(DATA DIVISION)

変数や外部IOに関して記述するところ。今回は空。

手続き部(PROCEDURE DIVISION)

いわゆるロジックを書くところ。今回はこのような形でHello worldを標準出力することを記載。

000090 MAIN.
000100     DISPLAY "Hello world!"  UPON CONSOLE.
000110     STOP RUN.

感想

友人が仕事でCOBOLをやり始めたみたいだったので、エイプリルフールネタで入門してみました。学生時代にポケコンでBasic書いてた時代を思い出して、懐かしい気持ちになりました。

なんだかんだコンテナ化できれば、過去の資産は活かせそうかなと思いましたが、やはり使うのはやめましょう。人類には早すぎます。

適当にプログラミング関係のことやグラブル関係のことをつぶやいてたりします。もしよければ@glassmonekeyをフォローしてくれるとうれしいです。

参考

下記をお借りしました。
* ランタイム
https://sourceforge.net/projects/open-cobol/files/gnucobol/3.0/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Growiの定期バックアップ - AWSを利用しない場合

はじめに

どうも!生産技術部のエンジニアです。growiの定期バックアップ方法を検討しましたので、参考までにどうぞ。

環境

  • CentOS : 7.6.1810
  • Docker-CE : 19.03.1
  • Docker Compose : 1.25.0
  • Growi :以下 versioon_info.png

定期バックアップ

バックアップ用のスクリプトを作成

バックアップは、mongodumpコマンドを利用し、アーカイブファイルを作成します。作成するアーカイブファイルのファイル名には、日時とgrowiのバージョン番号を付けました。バックアップファイルからの復元には、mongorestoreコマンドを利用します。バックアップは一週間経過したファイルから削除します。
※growi/docker-compose.ymlのFILE_UPLOADをlocalに設定している場合は、別途バックアップが必要だと思われます。mongodbにしておけば不要かどうかも謎です。

growi_backup.sh
#!/bin/sh

BKDIR=/srv/growi/data/backups

# Creating backup archive
docker exec -d growi_mongo_1 mongodump --archive=mongodb.archive
docker cp growi_mongo_1:mongodb.archive $BKDIR/$(date --date '1 day ago' "+%s_%Y_%m_%d_3.7.1_mongodb.archive")

# Create config backup archive

# Delete old backups
find $BKDIR -mtime +6 | xargs rm -rf

crontabでスクリプトを定期実行

crontabにバックアップを取りたい時刻を設定し、スクリプトを実行する。

# crontab -l
0 2 * * * /root/Docker/growi/growi_backup.sh CRON=1

:cherry_blossom:ご参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プロキシ環境下でDocker Composeを用いてGrowiを立ち上げる

:cherry_blossom:はじめに

どうも!生産技術部のエンジニアです。マニュアルドキュメント類を整理するため、growiを利用させていただきました。以下に、立ち上げ手順をまとめましたので参考にどうぞ。

:cherry_blossom:環境

  • CentOS : 7.6.1810
  • Docker-CE : 19.03.1
  • Docker Compose : 1.25.0
  • Growi :以下 versioon_info.png

:cherry_blossom:前提条件

git、docker、docker-composeの導入が実施済みであること。

:cherry_blossom:立ち上げ手順

1.growi-docker-composeをGitHubから取得

git clone https://github.com/weseek/growi-docker-compose.git growi

2.プロキシの設定

elastic-searchのプロキシ設定

elastic-searchのプラグインを導入する際に、プロキシの設定が必要な様です。

growi/elasticsearch/Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:6.6.1
LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
# 下記2行にご自身の環境に合わせたproxyの設定を追記してください。
RUN ES_JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8888 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8888" bin/elasticsearch-plugin install analysis-kuromoji
RUN ES_JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8888 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8888" bin/elasticsearch-plugin install analysis-icu

growiのプロキシ設定

apkを利用する際に設定が必要だと思われますが、確認はしていません。

growi/Dockerfile
FROM weseek/growi:3
LABEL maintainer Yuki Takei <yuki@weseek.co.jp>

# 下記2行を追記し、ご自身の環境に合わせたプロキシに書き換えてください。
ENV http_proxy 'proxy.example.com:8888'
ENV https_proxy 'proxy.example.com:8888'
ENV APP_DIR /opt/growi

# install dockerize
ENV DOCKERIZE_VERSION v0.6.1
USER root
RUN apk add --no-cache --virtual .dl-deps curl \
    && curl -sL https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
        | tar -xz -C /usr/local/bin \
    && apk del .dl-deps

WORKDIR ${APP_DIR}

(参考)crowiの場合

crowiを導入する場合は、上記の設定に加えて、npmのパッケージ導入する際のプロキシ設定が必要です。

crowi/Dockerfile
RUN npm -g config set proxy $http_proxy
RUN npm -g config set https-proxy $https_proxy

3.ポートおよびファイアウォールの設定

外部からアクセスできるように、127.0.0.1:3000:30003000:3000に書き換える。

growi/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 3000:3000    # localhost only by default
      :
      :

ファイアウォールの3000番ポートを開ける。

$ firewall-cmd --add-port=3000/tcp --zone=public --permanent
$ firewall-cmd --reload
success

4.各種設定

  • FILE_UPLOAD=local/mongodb
    growiページ上の添付ファイルを保存する先を設定。localは、growiのコンテナ内のローカルフォルダに保存、mongodbは、mongodbのDB上に保存されます。
  • MATHJAX=1
    数式を美しく表現できる。
growi/docker-compose.yml
version: '3'

services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 3000:3000    # localhost only by default
    links:
      - mongo:mongo
      - elasticsearch:elasticsearch
    depends_on:
      - mongo
      - elasticsearch
    environment:
      - MONGO_URI=mongodb://mongo:27017/growi
      - ELASTICSEARCH_URI=http://elasticsearch:9200/growi
      - PASSWORD_SEED=changeme
      - FILE_UPLOAD=mongodb   # activate this line if you use MongoDB GridFS rather than AWS
      # - FILE_UPLOAD=local     # activate this line if you use local storage of server rather than AWS
      - MATHJAX=1             # activate this line if you want to use MathJax
      # - PLANTUML_URI=http://  # activate this line and specify if you use your own PlantUML server rather than public plantuml.com
      # - HACKMD_URI=http://    # activate this line and specify HackMD server URI which can be accessed from GROWI client browsers
      # - HACKMD_URI_FOR_SERVER=http://hackmd:3000  # activate this line and specify HackMD server URI which can be accessed from this server container
      # - FORCE_WIKI_MODE='public'    # activate this line to force wiki public mode
      # - FORCE_WIKI_MODE='private'   # activate this line to force wiki private mode

    command: "dockerize
              -wait tcp://mongo:27017
              -wait tcp://elasticsearch:9200
              -timeout 60s
              npm run server:prod"
    restart: unless-stopped
    volumes:
      - growi_data:/data
      :
      :

5.起動

最後にdocker-composeで起動して終了です。起動後、しばらくしてからhttp://<サーバのIP>:3000にアクセスしサインインの画面が表示されれば完了です。

# Dockerの起動およびリロード
$ docker-compose up -d

:cherry_blossom:最後に

無事に立ち上がりました。バックアップ方法についても検討していきます。

:cherry_blossom:ご参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerコンテナ内でGitコマンドを実行しようとすると「Operation not permitted」というエラーが出るときの対応

環境

  • Windows10 Pro 64bit
  • VSCode 1.43.2
  • Docker for Desktop 2.2.0.4
  • VSCode Remote-Containers使用

エラー内容

bash-5.0# git status
fatal: failed to read object xxxxxxxxx: Operation not permitted

対処方法

エクスプローラーで該当プロジェクトの.gitフォルダのプロパティを開き、
Read-only (読み取り専用)のチェックを外すと問題なくGitコマンドが実行できるようになります。

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Desktop 裏技プロキシ設定

社内開発だとプロキシ環境下でDocker Desktopを利用することも多いと思います。

プロキシ設定で手こずったので、裏技かどうかわかりませんが
色んなところで応用が利くんじゃないかと思い、メモ代わりに投稿します。

http://host.docker.internal:3128

image.png

設定画面とは別に config.json ファイルを編集する。

「C:¥Users¥ユーザー名¥.docker¥config.json」

.dockerフォルダが生成されていない、または config.jsonファイルが生成されていない場合は自分で作成すればいい。

編集内容

config.json
{
    "proxies":
    {
        "default":
        {
            "httpProxy":"http://host.docker.internal:3128",
            "httpsProxy":"http://host.docker.internal:3128",
            "noProxy":"127.0.0.*"
        }
    }
}

編集後は、Docker Desktopを再起動してプロキシの設定を反映。(完了)

CNTLMと併用して利用しています。

CNTLMのダウンロード先はこちら
CNTLMの設定方法についてはこちら
CNTLM利用コマンド一覧はこちら

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mac上で動作するVirtualBoxのUbuntu上でGccクロスコンパイル環境をDockerで構築

Mac上で動作するVirtualBoxのUbuntu上でGccクロスコンパイル環境をDockerで構築

Gccのクロスコンパイル環境をMacOSで起動したVirtualbox上のUbuntuのDockerで構築しました。

なんでこんな複雑な構成にしているかというと、環境/作業によるかと思いますが、体感的にはMacOSのDockerより早い気がしているため。

構成

レイヤー 動作環境 OS
Docker Ubuntu18.04(ここに構築)
VirtualBox Ubuntu18.04
Macboo MacOS

Docker環境のインストール

リンク先を参照ください。
MacOS の VirtualBox Ubntu上にDocker環境を構築

VirtualBox上のUbuntu18.4環境

tagucchan@tagvbbuntugcc:~$ uname -a
Linux tagvbbuntugcc 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
tagucchan@tagvbbuntugcc:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic
tagucchan@tagvbbuntugcc:~$ sudo docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 01:25:46 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 01:24:19 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Ubuntu18.04イメージをプル

tagucchan@tagvbbuntugcc:~$ sudo docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
5bed26d33875: Pull complete 
f11b29a9c730: Pull complete 
930bda195c84: Pull complete 
78bf9a5ad49e: Pull complete 
Digest: sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392b7546b43a051853a341d
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
tagucchan@tagvbbuntugcc:~$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               4e5021d210f6        7 days ago          64.2MB
hello-world         latest              fce289e99eb9        15 months ago       1.84kB

Dockerでubuntu 18.04のコンテナを作成

tagucchan@tagvbbuntugcc:~$ sudo docker run -it -d --name gcc-ubuntu ubuntu:18.04 
af48c1a975a26096ca8b0df01bd33f1c083604916f23c562607635f03548deb0

tagucchan@tagvbbuntugcc:~$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
af48c1a975a2        ubuntu:18.04        "/bin/bash"         2 minutes ago       Up 2 minutes                            gcc-ubuntu

"/bin/bash”を起動

tagucchan@tagvbbuntugcc:~$ sudo docker exec -it gcc-ubuntu /bin/bash
root@af48c1a975a2:/# 

Dockerの基本的な使い方

docker ps -a
docker start <CONTAINER IDまたはNAME> # コンテナの起動
docker stop <CONTAINER IDまたはNAME> # コンテナの停止

クロスコンパイラのビルドに必要なパッケージをインストール

root@af48c1a975a2:/# apt-get update
root@af48c1a975a2:/# apt-get upgrade
root@af48c1a975a2:/# apt-get install build-essential texinfo gawk
root@af48c1a975a2:/# apt-get install vim-tiny less wget rsync

bisonをインストール

root@af48c1a975a2:/# mkdir ~/dev
root@af48c1a975a2:/# mkdir ~/dev/bison
root@af48c1a975a2:/# cd ~/dev/bison

GUN M4をインストール

root@af48c1a975a2:/# wget https://ftp.gnu.org/gnu/m4/m4-1.4.18.tar.gz
root@af48c1a975a2:/# tar xf m4-1.4.18.tar.gz
root@af48c1a975a2:/# cd m4-1.4.18
root@af48c1a975a2:/# ./configure --prefix=/usr/local
root@af48c1a975a2:/# make
root@af48c1a975a2:/# make install
root@af48c1a975a2:/# cd ..

bisonをインストール

root@af48c1a975a2:/# wget https://ftp.gnu.org/gnu/bison/bison-3.5.tar.gz
root@af48c1a975a2:/# tar xf bison-3.5.tar.gz
root@af48c1a975a2:/# cd bison-3.5
root@af48c1a975a2:/# ./configure --prefix=/usr/local
root@af48c1a975a2:/# make
root@af48c1a975a2:/# make install

VirtualBoxのUbuntu内Dockerの情報

root@af48c1a975a2:/# gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

root@af48c1a975a2:/# uname -a
Linux af48c1a975a2 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

root@af48c1a975a2:/# lsb_release -a
bash: lsb_release: command not found
root@af48c1a975a2:/# apt-get install lsb-release
root@af48c1a975a2:/# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

root@af48c1a975a2:/# /lib/x86_64-linux-gnu/libc
libc-2.27.so  libc.so.6     
root@af48c1a975a2:/# /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.3.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

物理 CPU、CPU コア、および論理 CPU の数を確認

root@af48c1a975a2:~/dev/xgcc# grep physical.id /proc/cpuinfo | sort -u | wc -l
1

シェルスクリプトを作成

root@af48c1a975a2:/# mkdir ~/dev/xgcc
root@af48c1a975a2:/# cd ~/dev/xgcc

シェルスクリプト(ソースコードをダウンロード)

root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_download.sh
xgcc_download.sh
#!/bin/bash

set -e

BINUTILS_VERSION=binutils-2.34
GCC_VERSION=gcc-7.3.0
LINUX_KERNEL_VERSION=linux-5.3
GLIBC_VERSION=glibc-2.27

wget -nc http://ftpmirror.gnu.org/binutils/$BINUTILS_VERSION.tar.gz
wget -nc http://ftpmirror.gnu.org/gcc/gcc-7.3.0/$GCC_VERSION.tar.gz
wget -nc https://www.kernel.org/pub/linux/kernel/v5.x/$LINUX_KERNEL_VERSION.tar.xz
wget -nc http://ftpmirror.gnu.org/glibc/$GLIBC_VERSION.tar.xz

for f in *.tar*; do
    tar xfk $f
done

cd $GCC_VERSION
./contrib/download_prerequisites

シェルスクリプト(ソースコードをビルド)

root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_build.sh
xgcc_build.sh
#!/bin/bash

set -e

PREFIX=/usr/local
# CPU 1
PARALLEL_MAKE=
#CONFIGURATION_OPTIONS="--disable-multilib --disable-nls"
CONFIGURATION_OPTIONS="--disable-multilib --disable-nls --disable-libmpx"

BINUTILS_VERSION=binutils-2.34
GCC_VERSION=gcc-7.3.0
LINUX_KERNEL_VERSION=linux-5.3
GLIBC_VERSION=glibc-2.27

build() {
    local TARGET="$1"
    local LINUX_ARCH="$2"

    # Step 1. Binutils
    mkdir -p build-binutils-$TARGET
    cd build-binutils-$TARGET
    ../$BINUTILS_VERSION/configure --prefix=$PREFIX --target=$TARGET $CONFIGURATION_OPTIONS
    make $PARALLEL_MAKE
    make install
    cd ..

    # Step 2. Linux Kernel Headers
    cd $LINUX_KERNEL_VERSION
    make ARCH=$LINUX_ARCH INSTALL_HDR_PATH=$PREFIX/$TARGET headers_install
    cd ..

    # Step 3. C/C++ Compilers
    mkdir -p build-gcc-$TARGET
    cd build-gcc-$TARGET
    ../$GCC_VERSION/configure --prefix=$PREFIX --target=$TARGET --enable-languages=c,c++ $CONFIGURATION_OPTIONS
    make $PARALLEL_MAKE gcc_cv_libc_provides_ssp=yes all-gcc
    make install-gcc
    cd ..

    # Step 4. Standard C Library Headers and Startup Files
    mkdir -p build-glibc-$TARGET
    cd build-glibc-$TARGET
    ../$GLIBC_VERSION/configure --prefix=$PREFIX/$TARGET --build=$MACHTYPE --host=$TARGET --target=$TARGET --with-headers=$PREFIX/$TARGET/include $CONFIGURATION_OPTIONS libc_cv_forced_unwind=yes
    make install-bootstrap-headers=yes install-headers
    make $PARALLEL_MAKE csu/subdir_lib
    install csu/crt1.o csu/crti.o csu/crtn.o $PREFIX/$TARGET/lib
    $TARGET-gcc -nostdlib -nostartfiles -shared -x c /dev/null -o $PREFIX/$TARGET/lib/libc.so
    touch $PREFIX/$TARGET/include/gnu/stubs.h
    cd ..

    # Step 5. Compiler Support Library
    cd build-gcc-$TARGET
    make $PARALLEL_MAKE all-target-libgcc
    make install-target-libgcc
    cd ..

    # Step 6. Standard C Library & the rest of Glibc
    cd build-glibc-$TARGET
    make $PARALLEL_MAKE
    make install
    cd ..

    # Step 7. Standard C++ Library & the rest of GCC
    cd build-gcc-$TARGET
    make $PARALLEL_MAKE all
    make install
    cd ..

    rm -rf build-binutils-$TARGET build-gcc-$TARGET build-glibc-$TARGET
}

build i686-linux-gnu x86
build arm-linux-gnueabi arm
build arm-linux-gnueabihf arm
build aarch64-linux-gnu arm64
build mips-linux-gnu mips
build mipsel-linux-gnu mips
build mips64el-linux-gnuabi64 mips

# 以下は私の環境ではエラーが出て駄目でした。
#build powerpc-linux-gnu powerpc
#build powerpc64le-linux-gnu powerpc
#build alpha-linux-gnu alpha
#build s390x-linux-gnu s390
#build m68k-linux-gnu m68k

#build x86_64-linux-gnu x86
#build sparc-linux-gnu sparc
#build sparc64-linux-gnu sparc
#build cris-linux-gnu cris
#build sh4-linux-gnu sh

シェルスクリプト(各アーキテクチャのbinutilsをビルド)

root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_build_all_binutils.sh
xgcc_build_all_binutils.sh
#!/bin/bash
# build-binutils-all.sh

set -e

PREFIX=/usr/local
# cpu 1
PARALLEL_MAKE=
CONFIGURATION_OPTIONS="--disable-multilib --disable-nls"

BINUTILS_VERSION=binutils-2.34

mkdir -p build_all_binutils
cd build_all_binutils
../$BINUTILS_VERSION/configure --prefix=$PREFIX --enable-targets=all --enable-64-bit-bfd --program-prefix=all- $CONFIGURATION_OPTIONS
make $PARALLEL_MAKE
make install
cd ..

rm -rf build_all_binutils

スクリプトを実行

要:かなりの時間とディスク容量

root@af48c1a975a2:~/dev/xgcc# cd ~/dev/xgcc
root@af48c1a975a2:~/dev/xgcc# bash xgcc_download.sh
root@af48c1a975a2:~/dev/xgcc# nohup bash xgcc_build.sh &
root@af48c1a975a2:~/dev/xgcc# nohup bash xgcc_build_all_binutils.sh &

動作テスト(MIPS向けのアセンブリ出力)

root@af48c1a975a2:~/dev/xgcc#  mkdir test
root@af48c1a975a2:~/dev/xgcc# cd test
root@af48c1a975a2:~/dev/xgcc/test# vi main.c
main.c
#include <stdio.h>
int main(void) {
   printf("Hellow");
   return 0;
}

実行例

root@af48c1a975a2:~/dev/xgcc/test# mips-linux-gnu-gcc -S main.c
root@af48c1a975a2:~/dev/xgcc/test# cat main.s 
    .file   1 "main.c"
    .section .mdebug.abi32
    .previous
    .nan    legacy
    .module fp=32
    .module nooddspreg
    .abicalls
    .text
    .rdata
    .align  2
$LC0:
    .ascii  "Hellow\000"
    .text
    .align  2
    .globl  main
    .set    nomips16
    .set    nomicromips
    .ent    main
    .type   main, @function
main:
    .frame  $fp,32,$31      # vars= 0, regs= 2/0, args= 16, gp= 8
    .mask   0xc0000000,-4
    .fmask  0x00000000,0
    .set    noreorder
    .set    nomacro
    addiu   $sp,$sp,-32
    sw  $31,28($sp)
    sw  $fp,24($sp)
    move    $fp,$sp
    lui $28,%hi(__gnu_local_gp)
    addiu   $28,$28,%lo(__gnu_local_gp)
    .cprestore  16
    lui $2,%hi($LC0)
    addiu   $4,$2,%lo($LC0)
    lw  $2,%call16(printf)($28)
    nop
    move    $25,$2
    .reloc  1f,R_MIPS_JALR,printf
1:  jalr    $25
    nop

    lw  $28,16($fp)
    move    $2,$0
    move    $sp,$fp
    lw  $31,28($sp)
    lw  $fp,24($sp)
    addiu   $sp,$sp,32
    jr  $31
    nop

    .set    macro
    .set    reorder
    .end    main
    .size   main, .-main
    .ident  "GCC: (GNU) 7.3.0"
root@af48c1a975a2:~/dev/xgcc/test# 

Dockerコンテナを停止

exit  # Ubuntuのshellから抜ける
docker stop gcc-ubuntu # コンテナが停止
docker ps -a # 停止確認
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.13 - TDDでPost機能をコーディング part2 -

はじめに

こんにちは!
今回は前回のPost機能のコーディングの続きです。ユーザー詳細ページでそのユーザーが投稿したポストに絞って表示させる機能をコーディングします。

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

前回の残り

まずは前回やり残したテストシナリオをもう一度確認しておきましょう。

  1. ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
  2. ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
  3. ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
  4. サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
  5. サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  6. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  7. 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  8. サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  9. サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  10. サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  11. サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

残り11シナリオ。今回は5シナリオをやっていきます!それでポストページが完了、あとはユーザー詳細ページにポストを表示するストーリーだけになります。

今回もコンテナを立ち上げてコンテナの中でコマンドを実行していきたいと思います。

$ docker-compose up -d
$ docker-compose exec web ash

ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること

今回もテストコードからです。「ポストする」ボタンにはpost_buttonidを付与するとしましょう。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること" do
      # テスト用のユーザーを作成する
+     user = create_user(1)
      # このテストシナリオで使うポスト内容として空文字を定義する
+     content = ""
      # このテストシナリオで期待するエラーメッセージを定義する
+     error_message = "ポストを入力してください"
      # テスト開始前のDB内のPostの数を記憶しておく
+     post_count = Post.count
      # userでサインインする
+     sign_in(user)
+ 
      # ポストページにアクセスする
+     visit posts_path
      # ポスト入力欄にcontentを入力する
+     fill_in :post_content, with: content
      # 投稿するボタン(#post_button)をクリックする
+     click_on :post_button
+ 
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
      # ページ内に期待するエラーメッセージが表示されていることを検証する
+     expect(page).to have_text error_message
      # DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する
+     expect(Post.count).to eq post_count
+   end
  end

検証内容としては特に「期待するエラーメッセージが表示されていること」と「DB内のPostの数に変化がないこと」を検証することでバリデーションが効いていることを確認しています。

では、テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
     Failure/Error: click_on :post_button

     Capybara::ElementNotFound:
       Unable to find link or button :post_button

Finished in 20.3 seconds (files took 7.34 seconds to load)
11 examples, 1 failure

#post_buttonがないので怒られました。ので、ボタンを追加しましょう。

app/views/posts/index.html.erb
  <div class="container my-5">
    <%= form_with model: @post, url: nil, local: true do |form| %>
      <div class="form-group">
        <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
      </div>
+     <div class="text-right">
+       <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
+     </div>
    <% end %>
  </div>

post_buttonを追加してみました。UI的には以下のような感じになっているはず!
image.png

OK。ボタンは探せるようになったはずなのでまたテスト。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
     Failure/Error: expect(page).to have_text error_message
       expected to find text "ポストを入力してください" in "The page you were looking for doesn't exist.\nYou may have mistyped the address or the page may have moved.\nIf you are the application owner check the logs for more information."

Finished in 11.97 seconds (files took 8.31 seconds to load)
11 examples, 1 failure

次はエラーメッセージがみつからないみたいです。確かにエラーメッセージを表示する機能は作っていない!

まずエラーメッセージを表示するためには、ポストを送信してバリデーションに引っかかり、そのエラーメッセージをポストページ、つまりposts/index.html.erbで表示できるようにする必要があります。

まずは、Postsコントローラーにcreateアクションを作成して、そのアクションにポストを送信できるようにルーティングとform_withを更新しましょう。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    def index
      redirect_to root_path unless signed_in?
      @post = Post.new
    end
+ 
+   def create
+     # 未サインインの場合、トップページにリダイレクトする
+     redirect_to root_path unless signed_in?
+     # Strong parameterからリクエスト内容の通りにPostモデルオブジェクトを作成する
+     @post = Post.new(post_params)
+     # Postモデルオブジェクトのuser_idにサインイン中のuserのidを定義する
+     @post.user = current_user
+ 
+     if @post.save
+       # Todo: DB保存が成功した場合の動作
+     else
+       # DB保存が失敗した場合、ポストページをレンダリングする
+       render :index
+     end
+   end
+ 
+   private
+     # PostのStrong parameter
+     def post_params
+       params.require(:post).permit(:content)
+     end
  end

@post.userはあえてフォームからの送信ではなく、サーバーサイドでcurrent_user、つまりサインイン中のユーザーを設定しています。フォームから送信されたユーザーのIDを設定する方法も考えられますが、フォームの内容はユーザー側で簡単に改変できてしまうので違うユーザーのポストとして登録されてしまう危険性があります。
そのため、今回はサーバー側で処理をするようにしてみました。

config/routes.rb
  Rails.application.routes.draw do
    ...
    get   '/posts', to: 'posts#index',  as: :posts
+   post  '/posts', to: 'posts#create', as: :create_post
  end
app/views/posts/index.html.erb
...
- <%= form_with model: @post, url: nil, local: true do |form| %>
+ <%= form_with model: @post, url: create_post_path, local: true do |form| %>
...

これでcreateアクションで@post.saveが失敗した場合、@postにエラーメッセージが格納されてindex.html.erbがレンダリングされるようになるはずです。

次に、@post.savecontentが未入力の場合にエラーになるようにPostモデルのcontent属性にpresenceを定義しましょう。

app/models/post.rb
  class Post < ApplicationRecord
    belongs_to :user
+
+   validates :content,
+     presence: true
  end

Postモデルにはpresenceのバリデーションを定義します。

最後にエラーメッセージを表示できるようにViewを更新します。

app/views/posts/index.html.erb
  <div class="container my-5">
+
+   <% if @post.errors.any? %>
+     <div class="alert alert-danger">
+       <ul class="mb-0">
+         <% @post.errors.full_messages.each do |msg| %>
+           <li><%= msg %></li>
+         <% end %>
+       </ul>
+     </div>
+   <% end %>
+
    <%= form_with model: @post, url: create_post_path, local: true do |form| %>
      <div class="form-group">
        <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
      </div>
      <div class="text-right">
        <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
      </div>
    <% end %>
  </div>

これでポストが未入力の場合にエラーメッセージが表示されるようになっているはずです。再びテストを実行してみます。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
     Failure/Error: expect(page).to have_text error_message
       expected to find text "ポストを入力してください" in "sample app\nPosts\nProfile\nSign out\nContentを入力してください\n(c) Hoge Inc. All Rights Reserved."

Finished in 19.22 seconds (files took 7.93 seconds to load)
11 examples, 1 failure

またテスト失敗理由が変わりました。
今度は「ポストを入力してください」というエラーメッセージを期待していたが、「Contentを入力してください」となってしまっていたようです。

属性の日本語表記はconfig/locales/ja.ymlで定義していますので、更新していきます。

config/locales/ja.yml
  ja:
    activerecord:
      attributes:
        user:
          name: "お名前"
          email: "メールアドレス"
          password: "パスワード"
          password_confirmation: "確認用パスワード"
+       post:
+         content: "ポスト"
      errors:
  ...

ここまででモデルのバリデーションとエラーメッセージの表示までの実装を終えました。ではテストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 19.99 seconds (files took 7.33 seconds to load)
11 examples, 0 failures

これでテストがGreenの状態になりました。

ここで少しリファクタリングしておきます。今、Postsコントローラーのindexアクションとcreateアクションの最初に未サインインであればトップページにリダイレクトする、全く同じ処理を書いてしまっています。
これではDRYではないので、メソッド化してbefore_actionで実行するようにリファクタします。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
+   before_action :redirect_to_root_unless_signed_in

    def index
-     redirect_to root_path unless signed_in?
      @post = Post.new
    end

    def create
-     redirect_to root_path unless signed_in?
      @post = Post.new(post_params)
      @post.user = current_user
      if @post.save
      else
        render :index
      end
    end

    private
      def post_params
        params.require(:post).permit(:content)
      end
+ 
+     # 未サインインの場合、トップページにリダイレクトするメソッド
+     def redirect_to_root_unless_signed_in
+       redirect_to root_path unless signed_in?
+     end
end

はい。before_actionにひとまとめにしてみました。

ではこれでもちゃんとテストがパスするか確認をしておきましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 18.5 seconds (files took 5.6 seconds to load)
11 examples, 0 failures

Greenな状態をキープしたままリファクタリングができましたね!

ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること

まずはテストです!

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること" do
+     # テスト用のユーザーを作成する
+     user = create_user(1)
+     # このテストシナリオで使うポスト内容として141文字を定義する    
+     content = "a" * 141
+     # このテストシナリオで期待するエラーメッセージを定義する
+     error_message = "ポストは140文字以内で入力してください"
+     # テスト開始前のDB内のPostの数を記憶しておく
+     post_count = Post.count
+     # userでサインインする
+     sign_in(user)
+  
+     # ポストページにアクセスする
+     visit posts_path
+     # ポスト入力欄にcontentを入力する
+     fill_in :post_content, with: content
+     # 投稿するボタン(#post_button)をクリックする
+     click_on :post_button
+ 
+     # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+     # ページ内に期待するエラーメッセージが表示されていることを検証する
+     expect(page).to have_text error_message
+     # ポスト入力欄に入力していたポスト内容がそのまま残っていることを検証する
+     expect(find("#post_content").value).to eq content
+     # DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する
+     expect(Post.count).to eq post_count
+   end
  end

テスト実行です。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
     Failure/Error: expect(page).to have_text error_message
       expected to find text "ポストは140文字以内で入力してください" in "sample app\nPosts\nProfile\nSign out\n(c) Hoge Inc. All Rights Reserved."

Finished in 27.16 seconds (files took 5.71 seconds to load)
12 examples, 1 failure

エラーメッセージを見つけられないため失敗しているようです。今、140文字の制限をPostモデルのcontent属性に付与していないのでPost.savetrueだったのでしょう。
バリデーションを追加してみます。

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  validates :content,
-   presence: true
+   presence: true,
+   length: { maximum: 140 }
end

では、またテストを実行してみます。

# rspec spec/system/07_posts_spec.rb

Finished in 22.78 seconds (files took 6.21 seconds to load)
12 examples, 0 failures

Greenになりました!

ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること

テストから。

spec/system/07_posts_spec.rb
  ...
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること" do
+     # テスト用のユーザーを作成する
+     user = create_user(1)
+     # このテストシナリオで使うポスト内容を4つ用意する
+     # "Hello, world.":  通常のポスト内容
+     # "a":              0文字がNGなので境界値として1文字のポスト内容を用意
+     # "a" * 140:        141文字がNGなので境界値として140文字のポスト内容を用意
+     # "Hello.\nWorld.": 特殊なケースとして改行が入っているポスト内容を用意
+     contents = ["Hello, world.", "a", "a" * 140, "Hello.\nWorld."]
+     # userでサインインする
+     sign_in(user)
+
+     # contentsの中から1つずつをテストする
+     contents.each do |content|
+       # テスト開始前のDB内のPostの数を記憶しておく
+       post_count = Post.count
+      
+       # ポストページにアクセスする
+       visit posts_path
+       # ポスト入力欄にcontentを入力する
+       fill_in :post_content, with: content
+       # 投稿するボタン(#post_button)をクリックする
+       click_on :post_button
+
+       # 現在のページがポストページであることを検証する
+       expect(current_path).to eq posts_path
+       # ポスト内容がクリアされていることを検証する
+       expect(find("#post_content").value).to eq ""
+       # DB内のPostが1つ増えている(=投稿したポストが保存された)ことを検証する
+       expect(Post.count).to eq post_count + 1
+       # ポストページのポスト一覧の一番上に投稿したポストが表示されていることを検証する
+       expect(find("#posts_list").all(".post-item").first).to have_text content
+       expect(find("#posts_list").all(".post-item").first).to have_text user.name
+     end
+   end
  end

あまりeachとかを使うとどのケースでテスト失敗したのか行数からはわかりにくくなってしまう危険性もあるのですが、長く書くのも面倒なので今回はいくつかの文字列をポストするテストをeachで繰り返してみました。
"a""a" * 140は境界値のテストをしています。
"Hello.\nWorld."は改行が入った時に正しく登録・表示されるかをテストするためのパターンです。

posts_listはポストページで過去のポストが一覧表示されるエリアのidとしてます。その中で各ポストにはpost-itemclassを割り当ててall(".post-item")でそのコレクションを取得できるようにしようと思います。.firstでその中でも一番最初に表示されるもの、つまり一番上に表示される要素を検証対象としています。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
     Failure/Error: expect(find("#post_content").value).to eq ""

       expected: ""
            got: "Hello, world."

       (compared using ==)

Finished in 22.11 seconds (files took 6.73 seconds to load)
13 examples, 1 failure

テストは失敗しています。
ポストが成功した場合はポストの入力エリアが空白に戻るようにしたいのですがそれができていないようです。

ポスト成功時の動作をposts#createでまだコーディングできていないので記述していきます。
単にposts#indexアクションにリダイレクトしてあげるだけで、indexアクションの中で@post = Post.newが実行されるのでcontentを初期化できそうです。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    ...
    def create
      @post = Post.new(post_params)
      @post.user = current_user
      if @post.save
+       redirect_to posts_path
      else
        render :index
      end
    end
    a...
  end

これで完成。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
     Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content

     Capybara::ElementNotFound:
       Unable to find css "#posts_list"

Finished in 26.85 seconds (files took 7.87 seconds to load)
13 examples, 1 failure

失敗理由が変わりました。#posts_listなんて要素ないよ、とのことなのでViewを作っていきましょう。

まずは#posts_listで表示するデータをコントローラー側で取得しておく必要がありますので、posts#indexアクションで全てのPostを作成日降順で取得するようにしましょう。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    ...
    def index
      @post = Post.new
+     # 更新日時降順で全てのポストを@postsに代入する
+     @posts = Post.order(created_at: :desc)
    end
    ...
  end

全てのポストを作成日時降順で@postsに入れてます。
続いてはこのインスタンス変数を使ってViewを作っていきます。

app/views/posts/index.html.erb
  <div class="container my-5">

    <% if @post.errors.any? %>
      <div class="alert alert-danger">
        <ul class="mb-0">
          <% @post.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
          <% end %>
        </ul>
      </div>
    <% end %>

    <%= form_with model: @post, url: create_post_path, local: true do |form| %>
      <div class="form-group">
        <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
      </div>
      <div class="text-right">
        <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
      </div>
    <% end %>
+
+   <div id="posts_list" class="my-5">
+     <% @posts.each do |post| %>
+       <div class="card post-item my-1">
+         <div class="card-body">
+           <h5 class="card-title"><%= post.user.name %></h5>
+           <p class="card-text"><%= post.content %></p>
+         </div>
+       </div>
+     <% end %>
+   </div>
  </div>

form_withよりも下にid=posts_listのフィールドを作って@postsをひとつずつ表示してます。
post.user.namepostの外部キーuser_idのUserモデルオブジェクトのnameを取得して表示してます。PostモデルとUserモデルを関連付けしているのでこういった使い方ができるんですね。

では、テストしてみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
     Failure/Error: expect(page).to have_text error_message
       expected to find text "ポストを入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information."

  2) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
     Failure/Error: expect(page).to have_text error_message
       expected to find text "ポストは140文字以内で入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information."

  3) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
     Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content
       expected to find text "Hello.\nWorld." in "John Smith\nHello. World."

Finished in 34 seconds (files took 5.62 seconds to load)
13 examples, 3 failures

うぉ、3つもテストが失敗している...
1)2)はなにやら例外が発生してしまっているようです。試しにdevelopment環境でポストを未入力で「ポストする」ボタンをクリックしてみましょう。
image.png
ふむふむ。<% @posts.each do |post| %>のところでundefined method 'each'が起きてますね。

この2つのテストはposts#createアクションで@post.savefalseの時にrender :indexでレンダリングさせているケースです。
もう一度コードをよくみると、このレンダリングまでにこのアクションでは@postsというインスタンス変数を定義していません。
今回の例外は@postsというモデルオブジェクトのインスタンス変数がないにもかかわらず@posts.eachを使おうとしていることから起きた例外と推測できます。

posts#createアクションを見直してみましょう。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    ...
    def create
      @post = Post.new(post_params)
      @post.user = current_user
      if @post.save
        redirect_to posts_path
      else
+       # 更新日時降順で全てのポストを@postsに代入する
+       @posts = Post.order(created_at: :desc)
        render :index
      end
    end
    ...
  end

@post.savefalseの場合、renderの前に@postsを定義するようにしてみました。
例外が解消されたかもう一度テストしてみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
     Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content
       expected to find text "Hello.\nWorld." in "John Smith\nHello. World."

Finished in 32.49 seconds (files took 7.4 seconds to load)
13 examples, 1 failure

ふー。今取り組んでいるテストシナリオだけがテスト失敗している状態に戻せましたね。
内容をみてみると"Hello.\nWorld."のポストが成功しているはずなのにページに表示されていないようです。
image.png
スクリーンショットをみると、確かに一番上のポストの文字列が改行されていないHello. World.という文字列になっていますね。

実はpost.contentのような書き方では文字列を表示することはできるのですが、改行がうまく表現できません。
これを解決するために、今回はsafe_joinメソッドを使って改行を正しく表示できるようにします。(参考: 「simple_format」や「safe_join」を使って、正常に改行表示させる方法(Rails) - りょうたくの技術ブログ

app/views/posts/index.html.erb
- <p class="card-text"><%= post.content %></p>
+ <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>

ちょっと書き方が複雑ですが、これでテキストエリアでつけた改行の通りに改行を表現することができるようになります。
こういったやり方はいろいろあるので、どう表現したいかによって使い分けが必要です。

では再度テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 30.57 seconds (files took 7.13 seconds to load)
13 examples, 0 failures

無事テストをパスすることができました!!
このように、テストで用いるデータの種類をいろいろなケース用意することで本当に期待通りの動作をしているかを正しく把握し実装することができるのです。

サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること

ここからはポストページの表示系ですね。
まずは投稿日時降順で表示ができているかという点です。複数ユーザーが投稿していることにします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # ポストを用意する
+     posts = []
+     posts.unshift Post.create(content: "first post", user: user1)
+     posts.unshift Post.create(content: "初めてのポスト", user: user2)
+     posts.unshift Post.create(content: "second post!!", user: user1)
+     # userでサインインする
+     sign_in(user1)
+
+     # ポストページにアクセスする
+     visit posts_path
+
+     # 投稿日時降順でポストが表示されていることを検証する
+     posts.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+     end
+   end
  end

今回は適当に3つのポストを生成しておき、作成日時降順でポストが表示されているかをチェックしています。
postsという空のArrayを最初に作成し、unshiftメソッドを使って配列の先頭にPost.createを挿入していきます。最終的には作成日時降順で配列にPostのモデルオブジェクトが格納されるかたちになります。(参考: Rubyで配列に要素を追加・挿入する:push, insert, unshift | UX MILK

これをeach_with_indexでモデルオブジェクトとindexを取り出して、posts_listi番目にある要素が新しい方からi番目に作成されたポストであるかどうかを検証しているというわけです。(参考: Rubyのeachでindexを取得する:each_with_index | UX MILK

では、テストを実行してみましょう。

# rpsec spec/system/07_posts_spec.rb

Finished in 30.64 seconds (files took 6.38 seconds to load)
14 examples, 0 failures

この辺りはすでに作り込みが終わっているのでテストがパスしていますね。

サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること

まずはテストです。ポストしたユーザーの名前を表示している要素にはpost-user-nameというclassをつけることにします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # ポストを用意する
+     posts = []
+     posts.unshift Post.create(content: "First Post!!", user: user1)
+     posts.unshift Post.create(content: "初めてのポスト", user: user2)
+     # user1でサインインする
+     sign_in(user1)
+
+     posts.each_with_index do |post, i|
+       # ポストページにアクセスする
+       visit posts_path
+       # 上からi番目のポストのユーザー名をクリックする
+       find("#posts_list").all(".post-item")[i].find(".post-user-name").click
+
+       # 現在のページがクリックしたポストのユーザーのユーザー詳細ページであることを検証する
+       expect(current_path).to eq user_path(post.user)
+     end
+   end
  end

いままではclick_onを使ってクリック操作を実装していましたが、要素.clickでも同じようにクリック操作ができます。click_onの場合はボタンやリンクに限定されていたのですが、clickの場合はどんな要素であれクリック操作ができるので必要になるケースもあります。覚えておいてくださいね。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in Created posts サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
     Failure/Error: find("#posts_list").all(".post-item")[i].find(".post-user-name").click

     Capybara::ElementNotFound:
       Unable to find css ".post-user-name" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[1]/DIV[1]/DIV[1]">

Finished in 35.66 seconds (files took 6.04 seconds to load)
15 examples, 1 failure

post-user-nameclassの要素が見つからないようですね。現在ポストしたユーザーの名前を表示している要素にpost-user-nameclassのリンクを定義しましょう。

app/views/posts/index.html.erb
- <h5 class="card-title post-user-name"><%= post.user.name %></h5>
+ <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>

link_toメソッドを使ってリンクを生成してみました。

# rspec spec/system/07_posts_spec.rb

Finished in 34.14 seconds (files took 5.9 seconds to load)
15 examples, 0 failures

テストパス!

まとめ

はい。今日はここまでです!
今回まででポストページの機能を実装することができましたね。
あとはユーザー詳細ページ側でそのユーザーのポストを表示するテストと機能を実装していきましょう!

ではまた次週!

後片付け

# exit
$ docker-compose down

本日のソースコード

Other Hands-on Links

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swagger-nodeとDockerで簡単にモックサーバーを構築する

はじめに

APIを呼び出す機能をテストする際に、とりあえずモックサーバーを立てたい。
そんなときに便利なのが、SwaggerのNode.js 製のモジュールであるswagger-nodeです。

SwaggerでAPIを定義しておくだけで、モックサーバーを起動できます。
便利なのは、自前でテストデータを用意しなくても、データ型(string, number, boolean, array, object etc)に応じた適当な値をレスポンスしてくれる点です。

Swaggerの概要やswagger-nodeの使い方はこちらで紹介されているため、詳細は割愛します。
Swaggerとswagger-node

この記事では、Node.jsのインストールが面倒なのと、環境を汚さずに使いたいということで、
swagger-nodeとDockerを組み合わせてモックサーバーを構築する方法を説明します。

前提

dockerとdocker-composeを利用できること

モックサーバーを構築する

Dockerfileの作成

まずはswagger-nodeを内包するDockerイメージを作成します。
最新のLTS版であるNode.js v12だとエラーになるようなので、v10を使用します。
https://github.com/swagger-api/swagger-node/issues/586
npm install --save swagger-routerすればOKとの情報もありましたが、未確認です。

適当なディレクトリでDockerfileを作成します。

FROM node:10

# アプリケーションディレクトリを作成する
WORKDIR /usr/src/app

# swagger-nodeをグローバルインストールする
RUN npm install swagger -g

ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 10; done"]

これをビルトしてDockerイメージを作成します。

$ docker build . -t swagger-node

swaggerプロジェクトの作成

swaggerプロジェクトを作成するために、swagger-nodeをインストールしたコンテナを起動して、swaggerコマンドを実行します。
作成したswaggerプロジェクトはホストにマウントするために、-v $PWD:/usr/src/appオプションを付けておきます。

$ docker run -d -v $PWD:/usr/src/app --name first-swagger-container swagger-node
$ docker exec -it first-swagger-container swagger project create sample-project
? Framework?
  connect
❯ express
  hapi
  restify
  sails

作成されたsample-projectはこのような構成になります。
swagger.yamlがAPI定義ファイルです。

sample-project/
 ├ api/
 │ ├ controllers/
 │ ├ helpers/
 │ ├ mocks/
 │ └ swagger
 │   └ swagger.yaml
 ├ config/
 ├ node_modules/
 ├ test/
 ├ .gitignore
 ├ app.js
 ├ package-lock.json
 ├ package.json
 └ README.md

なお、ここで作成したコンテナは不要なので削除します。

$ docker rm --force first-swagger-container

Dockerコンテナ起動

同じディレクトリにdocker-compose.yamlを作成して、swagger-mock(mockサーバーコンテナ)とswagger-editor(swagger.yamlを編集するためのコンテナ)を定義します。

docker-compose.yaml
version: "3.6"
services:
  swagger-mock:
    build: .
    environment:
      - CHOKIDAR_USEPOLLING=true
    ports:
      - "10010:10010"
    volumes:
      - .:/usr/src/app
    networks:
      examples-net:
        ipv4_address: 172.16.239.103
    # -m: モックモードとして起動するオプション
    entrypoint: bash -c "cd sample-project && swagger project start -m"
  swagger-editor:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/usr/src/app
    networks:
      examples-net:
        ipv4_address: 172.16.239.104
    # -s:     起動時にブラウザが立ち上がらないようにする
    # -p:     指定しない場合ポートがランダムになるため8000で固定する
    # --host: ipv4_addressで指定したIPを設定する
    entrypoint: bash -c "cd sample-project && swagger project edit -s -p 8000 --host 172.16.239.104"
networks:
  examples-net:
    name: examples-net
    driver: bridge
    ipam:
      driver: default
      config:
      - subnet: 172.16.239.0/24

swagger-mockのポイントとしては、CHOKIDAR_USEPOLLING=trueを設定している点です。
これは、マウントされているファイルが更新された際にコンテナを自動起動するための設定です。
これによって、swagger.yamlが更新された際に即時にモックサーバーに反映されるようになります。

  • 補足
    • swagger-nodeには、nodemonというモジュールが組み込まれており、ファイル更新を検知したら自動で再起動する仕組み(いわゆるホットリロード)が実装されています。 しかし、Dockerと組み合わせた場合にこれが効かなくなってしまい、Dockerコンテナとしてホットリロードする方法を採用しました。

それではDockerコンテナを起動してみます。

$ docker-compose up -d

コンテナが起動するとswagger-mockがモックサーバーとして利用できる状態になります。
試しにhttp://<コンテナのIPアドレス>:10010に対して、hello API(初回作成時のサンプルAPI)を呼ぶと、レスポンスを返してくれます。

$ curl  http://192.168.99.100:10010/hello?name=Scott
{"message":"Sample text"}

swagger.yamlを更新

目的とするAPIのモックサーバーを起動するには、swagger.yamlを編集する必要があります。
そこでswagger-editorコンテナを使用します。
ブラウザでhttp://<コンテナのIPアドレス>:8000にアクセスするとswagger.yamlの編集画面が開きます。

キャプチャ.PNG

試しに新しいAPIとして次のようにuser API(Get, POST)を定義してみます。
左側の編集パネルに次のように追記します。

swagger.yaml
swagger: "2.0"
  version: "0.0.1"
  title: Hello World App
# during dev, should point to your local machine
host: localhost:10010
# basePath prefixes all resource paths 
basePath: /
# 
schemes:
  # tip: remove http to make production-grade
  - http
  - https
# format of bodies a client can send (Content-Type)
consumes:
  - application/json
# format of the responses to the client (Accepts)
produces:
  - application/json
paths:
  ...(略)...
  /user:
    x-swagger-router-controller: UserController
    get:
      operationId: getUser
      parameters:
        - name: id
          in: query
          required: true
          type: string
      responses:
        "200":
          description: Success
          schema:
            properties:
              name:
                type: string
              age:
                type: number
    post:
      operationId: postUser
      parameters:
        - name: "user"
          in: "body"
          required: true
          schema:
            required:
              - name
              - age
            type: object
            properties:
              name:
                type: string
              age:
                type: number
      responses:
        "200":
          description: Success
          schema:
            properties:
              message:
                type: string
  ...(略)...

swagger.yamlの書き方について、基本的にはSwaggerの構文に従えばOKです。
ここで重要なのはx-swagger-router-controllerに任意の値を設定することです。
これはswagger-node独自の項目で、ルーティングするコントローラー(.js)を設定するものです。
swagger project startコマンドで普通に起動する場合は、ここで設定したコントローラー(.js)にルーティングされ、実装された処理に従ってレスポンスが返されます。
モックサーバーと起動する(-mオプションを付与する)場合は、コントローラーが存在しなくても、responsesで定義している型に応じてレスポンスを返してくれるのです。

動作確認

ブラウザの編集画面に書き込んだ時点で、swagger.yamlが更新され、上述したホットリロードの設定によってswagger-mockコンテナが再起動されます。
つまり、即時にモックサーバーに反映されています。
自身でコンテナを再起動する必要はありません。

実際にuser APIを呼んでみます。

$ curl http://192.168.99.100:10010/user?id=xxx
{"name":"Sample text","age":1}

$ curl -X POST -H 'Content-Type:application/json' -d '{"id": "aaa", "name": "hoge", "age": 10}' http://192.168.99.100:10010/user
{"message":"Sample text"}

適当な値が設定されたレスポンスを返してくれました。

所感

APIを呼び出す機能をテストしたい場合、Swagger定義をもらって編集画面を開いてコピペして、x-swagger-router-controllerという若干の設定を行えば、簡単にモックサーバーを起動できます。

あとは、自分でAPIを定義する場合にも、「swagger.yamlを編集→API呼び出しして確認」というライフサイクルを回しやすそうです。

おわりに

Dockerを勉強中なので、無駄な手順が含まれているように思います。(特にswaggerプロジェクトを作成するあたり)
おいおい勉強して更新していきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む