diff --git a/kadai2/hokita/README.md b/kadai2/hokita/README.md new file mode 100644 index 0000000..0cf196f --- /dev/null +++ b/kadai2/hokita/README.md @@ -0,0 +1,52 @@ +# 課題2 +## io.Readerとio.Writer +### io.Readerとio.Writerについて調べてみよう +- io.Readerとio.Writerについて調べてみよう + - 標準パッケージでどのように使われているか + - io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる + +### 解答 +#### 標準パッケージでどのように使われているか +- strings + - 文字列操作 +- bufio + - バッファリングしながら読み書きする +- bytes + - バイトスライスを操作する + +#### io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる +IOが統一されているため、標準入出力、ファイル、ネットワーク通信どのような場合でも、入出力処理をする側、それを使う側がお互いの処理内容を知らなくてすみ、付け替えも可能。 + +例えばファイルを読み込んで処理Aをするアプリがあり、今回新しくHTTPレスポンスからA処理をする機能を追加したい場合、io.Readerを返すHTTPレスポンス読込処理を用意すれば、処理A自体を変更する必要はない。また処理Aのテストは入力を意識する必要もない。 + +## テストを書いてみよう +### 1回目の課題のテストを作ってみて下さい + - テストのしやすさを考えてリファクタリングしてみる + - テストのカバレッジを取ってみる + - テーブル駆動テストを行う + - テストヘルパーを作ってみる + +### 対応 +#### テストのしやすさを考えてリファクタリングしてみる +- `io.Readerとio.Writer`の課題を通して`(c *Converter) Execute(in io.Reader, out io.Writer)`を作成した。 + +#### テストのカバレッジを取ってみる +```shell +$ go test -coverprofile=profile github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/ +ok github.com/gopherdojo/dojo8/kadai2/hokita/imgconv 0.438s coverage: 79.5% o +f statements + +$ go test -coverprofile=profile github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv +ok github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv 0.476s coverage: + 95.7% of statements +``` + +#### テーブル駆動テストを行う +- `map[string]struct`で作ってみた。 + +#### テストヘルパーを作ってみる +- convertで作成されたファイル確認、削除処理をする`checkAndDeleteFile`を作成 + +## わからなかったこと、むずかしかったこと +- モックをしたいがためにinterfaceを無理やり作ることはあるのだろうか。 +- だからと言って抽象化してモックしないとunitテストではなくE2Eテストになってしまいそう。 diff --git a/kadai2/hokita/convert.go b/kadai2/hokita/convert.go new file mode 100644 index 0000000..397c837 --- /dev/null +++ b/kadai2/hokita/convert.go @@ -0,0 +1,79 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + + "github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv" +) + +func convert(dir, from, to string) error { + if err := validation(dir, from, to); err != nil { + return err + } + + err := filepath.Walk(dir, + func(path string, info os.FileInfo, err error) (rerr error) { + if err != nil { + return err + } + + fromImage, err := imgconv.NewImage("." + from) + if err != nil { + return err + } + + toImage, err := imgconv.NewImage("." + to) + if err != nil { + return err + } + + // ignore unrelated file + if !fromImage.Has(filepath.Ext(path)) { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + out, err := os.Create(switchExt(path, "."+to)) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + rerr = err + } + }() + + converter := imgconv.NewConverter(toImage.GetEncoder()) + return converter.Execute(file, out) + }) + if err != nil { + return err + } + + return nil + +} + +func switchExt(path, to string) string { + ext := filepath.Ext(path) + return path[:len(path)-len(ext)] + to +} + +func validation(dir, from, to string) error { + if dir == "" { + return errors.New("please specify a directory") + } + + if f, err := os.Stat(dir); os.IsNotExist(err) || !f.IsDir() { + return errors.New("cannot find directory") + } + + return nil +} diff --git a/kadai2/hokita/go.mod b/kadai2/hokita/go.mod new file mode 100644 index 0000000..17037d9 --- /dev/null +++ b/kadai2/hokita/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo8/kadai2/hokita/imgconv + +go 1.14 diff --git a/kadai2/hokita/imgconv/encoder.go b/kadai2/hokita/imgconv/encoder.go new file mode 100644 index 0000000..5d5ab1b --- /dev/null +++ b/kadai2/hokita/imgconv/encoder.go @@ -0,0 +1,10 @@ +package imgconv + +import ( + "image" + "io" +) + +type Encoder interface { + execute(w io.Writer, Image image.Image) error +} diff --git a/kadai2/hokita/imgconv/helper_test.go b/kadai2/hokita/imgconv/helper_test.go new file mode 100644 index 0000000..136c540 --- /dev/null +++ b/kadai2/hokita/imgconv/helper_test.go @@ -0,0 +1,18 @@ +package imgconv + +import ( + "os" + "testing" +) + +func checkAndDeleteFile(t *testing.T, file string) { + t.Helper() + + if _, err := os.Stat(file); err != nil { + t.Errorf(`"%v" was not found`, file) + } + + if err := os.Remove(file); err != nil { + t.Fatal(err) + } +} diff --git a/kadai2/hokita/imgconv/image.go b/kadai2/hokita/imgconv/image.go new file mode 100644 index 0000000..cbd338c --- /dev/null +++ b/kadai2/hokita/imgconv/image.go @@ -0,0 +1,24 @@ +package imgconv + +import ( + "errors" +) + +type Image interface { + GetEncoder() Encoder + Has(ext string) bool +} + +func NewImage(ext string) (Image, error) { + var jpeg JPEG + var png PNG + + switch { + case jpeg.Has(ext): + return &jpeg, nil + case png.Has(ext): + return &png, nil + } + + return nil, errors.New("selected extension is not supported") +} diff --git a/kadai2/hokita/imgconv/image_test.go b/kadai2/hokita/imgconv/image_test.go new file mode 100644 index 0000000..e08f263 --- /dev/null +++ b/kadai2/hokita/imgconv/image_test.go @@ -0,0 +1,46 @@ +package imgconv + +import ( + "reflect" + "testing" +) + +func TestNewImage(t *testing.T) { + tests := map[string]struct { + ext string + want Image + wantErrMsg string + }{ + "jpg": { + ext: ".jpg", + want: &JPEG{}, + }, + "png": { + ext: ".png", + want: &PNG{}, + }, + "txt": { + ext: ".txt", + wantErrMsg: "selected extension is not supported", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := NewImage(test.ext) + if !reflect.DeepEqual(got, test.want) { + t.Errorf( + `ext="%v" want="%v" got="%v"`, + test.ext, test.want, got, + ) + } + + if err != nil && err.Error() != test.wantErrMsg { + t.Errorf( + `ext="%v" wantErrMsg="%v" errMsg="%v"`, + test.ext, test.wantErrMsg, err.Error(), + ) + } + }) + } +} diff --git a/kadai2/hokita/imgconv/imgconv.go b/kadai2/hokita/imgconv/imgconv.go new file mode 100644 index 0000000..b81d5ea --- /dev/null +++ b/kadai2/hokita/imgconv/imgconv.go @@ -0,0 +1,23 @@ +package imgconv + +import ( + "image" + "io" +) + +type Converter struct { + encoder Encoder +} + +func NewConverter(encoder Encoder) *Converter { + return &Converter{encoder} +} + +func (c *Converter) Execute(in io.Reader, out io.Writer) error { + img, _, err := image.Decode(in) + if err != nil { + return err + } + + return c.encoder.execute(out, img) +} diff --git a/kadai2/hokita/imgconv/imgconv_test.go b/kadai2/hokita/imgconv/imgconv_test.go new file mode 100644 index 0000000..b05dc25 --- /dev/null +++ b/kadai2/hokita/imgconv/imgconv_test.go @@ -0,0 +1,30 @@ +package imgconv + +import ( + "bytes" + "image" + "io" + "os" + "testing" +) + +type mockEncoder struct{} + +func (e *mockEncoder) execute(w io.Writer, Image image.Image) error { + return nil +} + +func TestConverter_Execute(t *testing.T) { + file, err := os.Open("../testdata/test2/gopher.jpg") + if err != nil { + t.Fatal(err) + } + stdout := new(bytes.Buffer) + + converter := NewConverter(&mockEncoder{}) + + err = converter.Execute(file, stdout) + if err != nil { + t.Errorf("failed to call Execute(): %s", err) + } +} diff --git a/kadai2/hokita/imgconv/jpeg.go b/kadai2/hokita/imgconv/jpeg.go new file mode 100644 index 0000000..f3158e8 --- /dev/null +++ b/kadai2/hokita/imgconv/jpeg.go @@ -0,0 +1,31 @@ +package imgconv + +import ( + "image" + "image/jpeg" + "io" +) + +type JPEG struct{} + +func (*JPEG) GetEncoder() Encoder { + return &JPEGEncoder{} +} + +func (*JPEG) Has(ext string) bool { + var jpegExt = map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".JPG": true, + ".JPEG": true, + } + + return jpegExt[ext] +} + +type JPEGEncoder struct{} + +func (*JPEGEncoder) execute(w io.Writer, Image image.Image) error { + err := jpeg.Encode(w, Image, &jpeg.Options{Quality: jpeg.DefaultQuality}) + return err +} diff --git a/kadai2/hokita/imgconv/jpeg_test.go b/kadai2/hokita/imgconv/jpeg_test.go new file mode 100644 index 0000000..7574588 --- /dev/null +++ b/kadai2/hokita/imgconv/jpeg_test.go @@ -0,0 +1,55 @@ +package imgconv + +import ( + "image" + "os" + "reflect" + "testing" +) + +func TestJPEG_GetEncoder(t *testing.T) { + test := struct { + want Encoder + }{ + want: &JPEGEncoder{}, + } + + var jpeg JPEG + got := jpeg.GetEncoder() + if !reflect.DeepEqual(got, test.want) { + t.Errorf(`want="%v" got="%v"`, test.want, got) + } +} + +func TestJpegEncoder_execute(t *testing.T) { + inFile := "../testdata/test3/gopher.png" + outFile := "../testdata/test3/gopher.jpg" + + file, err := os.Open(inFile) + if err != nil { + t.Fatal(err) + } + + out, err := os.Create(outFile) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := out.Close(); err != nil { + t.Fatal(err) + } + }() + + img, _, err := image.Decode(file) + if err != nil { + t.Fatal(err) + } + + var encoder JPEGEncoder + err = encoder.execute(out, img) + if err != nil { + t.Errorf("failed to call execute(): %v", err) + } + + checkAndDeleteFile(t, outFile) +} diff --git a/kadai2/hokita/imgconv/png.go b/kadai2/hokita/imgconv/png.go new file mode 100644 index 0000000..037e3d9 --- /dev/null +++ b/kadai2/hokita/imgconv/png.go @@ -0,0 +1,29 @@ +package imgconv + +import ( + "image" + "image/png" + "io" +) + +type PNG struct{} + +func (*PNG) GetEncoder() Encoder { + return &PNGEncoder{} +} + +func (*PNG) Has(ext string) bool { + var pngExt = map[string]bool{ + ".png": true, + ".PNG": true, + } + + return pngExt[ext] +} + +type PNGEncoder struct{} + +func (*PNGEncoder) execute(w io.Writer, Image image.Image) error { + err := png.Encode(w, Image) + return err +} diff --git a/kadai2/hokita/imgconv/png_test.go b/kadai2/hokita/imgconv/png_test.go new file mode 100644 index 0000000..04e8b2b --- /dev/null +++ b/kadai2/hokita/imgconv/png_test.go @@ -0,0 +1,55 @@ +package imgconv + +import ( + "image" + "os" + "reflect" + "testing" +) + +func TestPNG_GetEncoder(t *testing.T) { + test := struct { + want Encoder + }{ + want: &PNGEncoder{}, + } + + var pngImage PNG + got := pngImage.GetEncoder() + if !reflect.DeepEqual(got, test.want) { + t.Errorf(`want="%v" got="%v"`, test.want, got) + } +} + +func TestPngEncoder_execute(t *testing.T) { + inFile := "../testdata/test4/gopher.jpg" + outFile := "../testdata/test4/gopher.png" + + file, err := os.Open(inFile) + if err != nil { + t.Fatal(err) + } + + out, err := os.Create(outFile) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := out.Close(); err != nil { + t.Fatal(err) + } + }() + + img, _, err := image.Decode(file) + if err != nil { + t.Fatal(err) + } + + var encoder PNGEncoder + err = encoder.execute(out, img) + if err != nil { + t.Errorf("failed to call execute(): %v", err) + } + + checkAndDeleteFile(t, outFile) +} diff --git a/kadai2/hokita/main.go b/kadai2/hokita/main.go new file mode 100644 index 0000000..a405761 --- /dev/null +++ b/kadai2/hokita/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +const ( + ExitCodeOk = 0 + ExitCodeError = 1 +) + +var from, to string + +func init() { + flag.StringVar(&from, "from", "jpg", "Conversion source extension.") + flag.StringVar(&to, "to", "png", "Conversion target extension.") +} + +func main() { + flag.Parse() + exitCode := run(flag.Arg(0)) + os.Exit(exitCode) +} + +func run(arg string) int { + err := convert(arg, from, to) + + if err != nil { + fmt.Fprintln(os.Stderr, err) + return ExitCodeError + } + + fmt.Println("Conversion finished!") + return ExitCodeOk +} diff --git a/kadai2/hokita/main_test.go b/kadai2/hokita/main_test.go new file mode 100644 index 0000000..a32209b --- /dev/null +++ b/kadai2/hokita/main_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "os" + "testing" +) + +func TestRun(t *testing.T) { + tests := map[string]struct { + dir string + from string + to string + want int + }{ + "success": { + dir: "testdata/test1/", + from: "jpg", + to: "png", + want: ExitCodeOk, + }, + "non exit dir": { + dir: "testdata/test99/", + from: "jpg", + to: "png", + want: ExitCodeError, + }, + "error": { + from: "jpg", + to: "png", + want: ExitCodeError, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + flag.CommandLine.Set("from", test.from) + flag.CommandLine.Set("to", test.to) + + if got := run(test.dir); got != test.want { + t.Errorf( + `dir="%v" from="%v" to="%v" want="%v" actual="%v"`, + test.dir, test.from, test.to, test.want, got, + ) + } + + if test.want != ExitCodeOk { + return + } + + outFile := "testdata/test1/gopher.png" + if err := os.Remove(outFile); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/kadai2/hokita/profile b/kadai2/hokita/profile new file mode 100644 index 0000000..33a37e8 --- /dev/null +++ b/kadai2/hokita/profile @@ -0,0 +1,48 @@ +mode: set +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:11.42,12.50 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:16.2,17.63 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:56.2,56.16 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:60.2,60.12 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:12.50,14.3 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:17.63,18.18 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:22.4,23.18 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:27.4,28.18 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:33.4,33.42 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:37.4,38.18 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:41.4,44.18 3 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:47.4,47.17 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:53.4,54.39 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:18.18,20.5 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:23.18,25.5 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:28.18,30.5 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:33.42,35.5 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:38.18,40.5 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:44.18,46.5 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:47.17,48.39 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:48.39,50.6 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:56.16,58.3 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:64.40,67.2 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:69.45,70.15 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:74.2,74.62 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:78.2,78.12 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:70.15,72.3 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/convert.go:74.62,76.3 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/main.go:16.13,19.2 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/main.go:21.13,25.2 3 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/main.go:27.26,30.16 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/main.go:35.2,36.19 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/main.go:30.16,33.3 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/image.go:12.42,16.9 3 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/image.go:23.2,23.63 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/image.go:17.21,18.20 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/image.go:19.20,20.19 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/imgconv.go:12.47,14.2 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/imgconv.go:16.64,18.16 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/imgconv.go:22.2,22.36 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/imgconv.go:18.16,20.3 1 0 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/jpeg.go:11.35,13.2 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/jpeg.go:15.35,24.2 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/jpeg.go:28.67,31.2 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/png.go:11.34,13.2 1 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/png.go:15.34,22.2 2 1 +github.com/gopherdojo/dojo8/kadai2/hokita/imgconv/imgconv/png.go:26.66,29.2 2 1 diff --git a/kadai2/hokita/testdata/test1/gopher.jpg b/kadai2/hokita/testdata/test1/gopher.jpg new file mode 100644 index 0000000..b712b25 Binary files /dev/null and b/kadai2/hokita/testdata/test1/gopher.jpg differ diff --git a/kadai2/hokita/testdata/test2/gopher.jpg b/kadai2/hokita/testdata/test2/gopher.jpg new file mode 100644 index 0000000..b712b25 Binary files /dev/null and b/kadai2/hokita/testdata/test2/gopher.jpg differ diff --git a/kadai2/hokita/testdata/test3/gopher.png b/kadai2/hokita/testdata/test3/gopher.png new file mode 100644 index 0000000..6010b73 Binary files /dev/null and b/kadai2/hokita/testdata/test3/gopher.png differ diff --git a/kadai2/hokita/testdata/test4/gopher.jpg b/kadai2/hokita/testdata/test4/gopher.jpg new file mode 100644 index 0000000..b712b25 Binary files /dev/null and b/kadai2/hokita/testdata/test4/gopher.jpg differ