Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kadai4-torotake #37

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kadai4/torotake/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode/
profile
48 changes: 48 additions & 0 deletions kadai4/torotake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Gopher道場#6 課題4

## おみくじAPIを作ってみよう

* JSON形式でおみくじの結果を返す
* 正月(1/1-1/3)だけ大吉にする
* ハンドラのテストを書いてみる

### ビルド

```
$ go build -o omikuji main.go
```

### Usage

```sh
$ omikuji <options>

options
-p [ポート番号] : listenポート番号 (デフォルト:8080)
```

* Ctrl+Cでサーバー終了
* / へのGETアクセスでランダムにおみくじ結果をjsonで返す
* 1/1〜1/3は必ず大吉が返る

----

## テスト実行

```sh
$ go test -v github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji
=== RUN TestServer_Handler
=== RUN TestServer_Handler/正月期間のときは全部大吉_開始境界_(1/1_00:00:00)
=== RUN TestServer_Handler/正月期間のときは全部大吉_終了境界_(1/3_23:59:59.999999999)
=== RUN TestServer_Handler/正月期間のときは全部大吉_開始境界直前_(12/31_23:59:59.999999999)
=== RUN TestServer_Handler/正月期間のときは全部大吉_終了境界直後_(1/4_00:00:00)
=== RUN TestServer_Handler/正月期間以外の時にランダム
--- PASS: TestServer_Handler (0.01s)
--- PASS: TestServer_Handler/正月期間のときは全部大吉_開始境界_(1/1_00:00:00) (0.00s)
--- PASS: TestServer_Handler/正月期間のときは全部大吉_終了境界_(1/3_23:59:59.999999999) (0.00s)
--- PASS: TestServer_Handler/正月期間のときは全部大吉_開始境界直前_(12/31_23:59:59.999999999) (0.00s)
--- PASS: TestServer_Handler/正月期間のときは全部大吉_終了境界直後_(1/4_00:00:00) (0.00s)
--- PASS: TestServer_Handler/正月期間以外の時にランダム (0.00s)
PASS
ok github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji 0.020s
```
27 changes: 27 additions & 0 deletions kadai4/torotake/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"flag"
"fmt"
"net/http"
"os"

"github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji"
)

var port int

func init() {
// -p=[ポート番号] default : 8080
flag.IntVar(&port, "p", 8080, "listen port number")
flag.Parse()
}

func main() {
server := omikuji.Server{}
http.HandleFunc("/", server.Handler)
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}
48 changes: 48 additions & 0 deletions kadai4/torotake/pkg/omikuji/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Package omikuji はおみじくサーバーの機能を提供します。
サーバー実行環境のLocalの時刻で1/1〜1/3は必ず大吉が返ります。

HTTPハンドラ部分の実装
*/
package omikuji

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)

// Server おみくじのHTTPサーバー用の構造体
type Server struct {
// おみくじ抽選結果の元になる時刻を取得する関数。nilの場合は現在時刻が使われる
GetTimeFunc func() time.Time
}

// Handler おみくじAPIのhttp handler
func (s *Server) Handler(w http.ResponseWriter, r *http.Request) {
var t time.Time
if s.GetTimeFunc != nil {
// 時刻取得関数が提供されていればそちらを使う
t = s.GetTimeFunc()
} else {
// デフォルトは現在時刻
t = time.Now()
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")

// おみくじ抽選
lot := draw(t)

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(lot); err != nil {
// jsonエンコードに失敗 Internal Server Errorとして返す
http.Error(w, "Internal Server Error", 500)
return
}

fmt.Fprint(w, buf.String())
}
118 changes: 118 additions & 0 deletions kadai4/torotake/pkg/omikuji/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package omikuji_test

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji"
)

func TestServer_Handler(t *testing.T) {
cases := []struct {
name string // テスト名
getTimeFunc func() time.Time // Serverに渡す時刻取得関数
expectedAllMatch bool // 全施行のおみくじ結果が一致することを正とするかどうか
expectedFortune string // 全思考のおみくじ結果が一致することを期待する場合、その結果
}{
{
name: "正月期間のときは全部大吉 開始境界 (1/1 00:00:00)",
getTimeFunc: func() time.Time { return time.Date(2019, time.January, 1, 0, 0, 0, 0, time.Local) },
expectedAllMatch: true,
expectedFortune: "大吉",
},
{
name: "正月期間のときは全部大吉 終了境界 (1/3 23:59:59.999999999)",
getTimeFunc: func() time.Time { return time.Date(2019, time.January, 3, 23, 59, 59, 999999999, time.Local) },
expectedAllMatch: true,
expectedFortune: "大吉",
},
{
name: "正月期間のときは全部大吉 開始境界直前 (12/31 23:59:59.999999999)",
getTimeFunc: func() time.Time { return time.Date(2018, time.December, 31, 23, 59, 59, 999999999, time.Local) },
expectedAllMatch: false,
expectedFortune: "",
},
{
name: "正月期間のときは全部大吉 終了境界直後 (1/4 00:00:00)",
getTimeFunc: func() time.Time { return time.Date(2019, time.January, 4, 0, 0, 0, 0, time.Local) },
expectedAllMatch: false,
expectedFortune: "",
},
{
// TODO : 本当の正月に実行したらひっかかってしまう…
name: "正月期間以外の時にランダム",
getTimeFunc: nil,
expectedAllMatch: false,
expectedFortune: "",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
runHandlerTest(t, c.getTimeFunc, c.expectedAllMatch, c.expectedFortune)
})
}
}

func runHandlerTest(t *testing.T, getTimeFunc func() time.Time, expectedAllMatch bool, expectedFortune string) {
t.Helper()
n := 100
var first string
var detectRandom bool
for i := 0; i < n; i++ {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)

s := omikuji.Server{
GetTimeFunc: getTimeFunc,
}
s.Handler(w, r)
rw := w.Result()
defer rw.Body.Close()

if rw.StatusCode != http.StatusOK {
t.Errorf("unexpected status code")
}

b, err := ioutil.ReadAll(rw.Body)
if err != nil {
t.Errorf("unexpected error : reading response body failed")
}

var d = map[string]interface{}{}
err = json.Unmarshal(b, &d)
if err != nil {
t.Errorf("unexpected error : unmarshaling json failed")
}

fortune, _ := d["fortune"].(string)
if expectedAllMatch {
// 全一致期待のときは期待値と違うのが返ってきた時点でエラー
if fortune != expectedFortune {
t.Fatalf("unexpected error : loop=%d, expect=%s, actual=%s", i, expectedFortune, fortune)
}
} else {
// ランダム期待のときは全部結果が一緒であればエラー
if i == 0 {
// 初回の値を記憶
first = fortune
} else if i == n-1 {
// 最後にチェック
if !detectRandom {
// 全部結果が一緒でランダムではなかった
// 本当にランダムで試行が全部同じ値になったときは諦める
t.Errorf("unexpected error : loop=%d, all result is same, not random. actual=%s", i, fortune)
}
} else {
// 違うのが出てきたらランダムという事にする
if fortune != first {
detectRandom = true
}
}
}
}
}
57 changes: 57 additions & 0 deletions kadai4/torotake/pkg/omikuji/omikuji.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Package omikuji はおみじくサーバーの機能を提供します。
サーバー実行環境のLocalの時刻で1/1〜1/3は必ず大吉が返ります。

おみじく抽選部分の実装
*/
package omikuji

import (
crand "crypto/rand"
"math"
"math/big"
"math/rand"
"time"
)

// Omikuji おみくじの結果を表す型
type omikuji struct {
Fortune string `json:"fortune"`
Message string `json:"message"`
}

// おみくじの定義リスト 0番目を大吉とする
var omikujiList = []omikuji{
{"大吉", "今のあなたは運がいい!今なら何でも出来る…かも?"},
{"吉", "結構ついてます。中吉より上だよ、知ってた?"},
{"中吉", "何事もほどほど。運もほどほど。"},
{"小吉", "小さい幸せを噛み締めましょう。"},
{"末吉", "末広がり!後々良いことあるかもよ。"},
{"凶", "しばらく大人しくしておいた方がいいかも……?"},
{"大凶", "これ以上悪くなることはないよ。どんまい!"},
}

func init() {
// 乱数シードの初期化
seed, _ := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
rand.Seed(seed.Int64())
}

func draw(t time.Time) omikuji {
// 1/1〜1/3は大吉固定
if isDaikichiDay(t) {
return omikujiList[0]
}

// 通常はランダム選択
index := rand.Intn(len(omikujiList))
return omikujiList[index]
}

func isDaikichiDay(t time.Time) bool {
// 1/1〜1/3は大吉固定
if t.Month() == time.January && (t.Day() >= 1 && t.Day() <= 3) {
return true
}
return false
}