diff --git a/kadai2/wataboru/.gitignore b/kadai2/wataboru/.gitignore new file mode 100644 index 0000000..090a1f0 --- /dev/null +++ b/kadai2/wataboru/.gitignore @@ -0,0 +1,2 @@ +.idea +.DS_Store diff --git a/kadai2/wataboru/README.md b/kadai2/wataboru/README.md new file mode 100644 index 0000000..e53a293 --- /dev/null +++ b/kadai2/wataboru/README.md @@ -0,0 +1,110 @@ +# 課題 2 + +## 【TRY】io.Readerとio.Writer + +### io.Readerとio.Writerについて調べてみよう + +- 標準パッケージでどのように使われているか +- io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる + +> - 標準パッケージでどのように使われているか + +- 実装している標準パッケージ + - bufio + - bytes + - zip + - strings + +- どの様に使われているか + - strings + +``` +func (w *appendSliceWriter) Write(p []byte) (int, error) { + *w = append(*w, p...) + return len(p), nil +} +``` + + - bufio + +``` +func (b *Writer) Write(p []byte) (nn int, err error) { + for len(p) > b.Available() && b.err == nil { + var n int + if b.Buffered() == 0 { + // Large write, empty buffer. + // Write directly from p to avoid copy. + n, b.err = b.wr.Write(p) + } else { + n = copy(b.buf[b.n:], p) + b.n += n + b.Flush() + } + nn += n + p = p[n:] + } + if b.err != nil { + return nn, b.err + } + n := copy(b.buf[b.n:], p) + b.n += n + nn += n + return nn, nil +} +``` + + - zip + +``` +func (w *fileWriter) Write(p []byte) (int, error) { + if w.closed { + return 0, errors.New("zip: write to closed file") + } + w.crc32.Write(p) + return w.rawCount.Write(p) +} +``` + +> - io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる + +1. 各パッケージで扱っている書き込み先は全て違うが、同じインターフェースにて抽象化されていること中の実装は隠蔽され、利用者としてはそれぞれ全て等しく `Write` や `Read` で利用することができる。 +2. 上記の通り隠蔽されることで、書き込み先や読み込み先という仕様が変更になったとしても、利用するパッケージを変更するだけで実現が可能になる。(変更容易性が上がる) + +## 【TRY】テストを書いてみよう + +### 1回目の課題のテストを作ってみて下さい +- テストのしやすさを考えてリファクタリングしてみる +- テストのカバレッジを取ってみる +- テーブル駆動テストを行う +- テストヘルパーを作ってみる + +## 動作手順 + +``` +$ go test ./imageconverter +$ go test ./commands/imageconverter +``` + +## カバレッジ + +- imageconverter +`./imageconverter_coveage.html` + +- main +`./imageconverter_coveage.html` + +## 感想等 + +### io.Readerとio.Writerについて調べてみよう + +- Interfaceを利用した + +### 【TRY】テストを書いてみよう + +- 画像のエンコード処理を、ストラテジーパターンを利用してリファクタリングしました。 +- 可能な限り、テスト対象の関数内で利用しているパッケージをMockして、依存を減らす様にしました。(Mockのパッケージを使わないで実現するのに相当苦労しました。) +- 標準パッケージや自身の実装した関数のmockにパッケージ変数とinitを利用していますが、これで正しいか悩んでいます。 +- os.FileInfoはInterfaceのため、test側でモックを実装しています。が、不要なメソッドも実装する必要があり苦労しました。埋め込みを用いた上で該当するメソッドだけ修正することは可能なのでしょうか。 +- mainのテストにて、flag.Parse() をinitの中で実行していたため、testでflag引数を渡すことができず苦戦しました。こちら、run()側にflag.Parse() を渡すのか、 testing.Init() を呼び出すか、どちらが良いか悩んでいます。あまりテストコードがメインの実装に漏れるのが嫌なので、他の方法があれば知りたいです。 +- テスタブルなコードを目指すあまり、実装として読みにくくなっていないか気になっています。 + diff --git a/kadai2/wataboru/commands/imageconverter/main.go b/kadai2/wataboru/commands/imageconverter/main.go new file mode 100644 index 0000000..69d5693 --- /dev/null +++ b/kadai2/wataboru/commands/imageconverter/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "flag" + "fmt" + "os" + "testing" + + "github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter" +) + +const ( + // ExitCodeSuccess is the exit code on success + ExitCodeSuccess = 0 + // ExitCodeError is the exit code when failed + ExitCodeError = 1 + // ExitCodeError is the exit code when failed + ExitCodeInvalidDirectoryError = 2 +) + +var ( + args imageconverter.Args + osStat func(name string) (os.FileInfo, error) + osIsNotExist func(err error) bool + imgconv func(args imageconverter.Args) error +) + +func init() { + testing.Init() + flag.StringVar(&args.Directory, "dir", "", "Input target Directory.\n ex) `--dir=./convert_image`") + flag.StringVar(&args.Directory, "d", "", "Input target Directory. (short)") + flag.StringVar(&args.BeforeExtension, "before", "jpg", "Input extension before conversion.\n ex) `--before=png`") + flag.StringVar(&args.BeforeExtension, "b", "jpg", "Input extension before conversion. (short)") + flag.StringVar(&args.AfterExtension, "after", "png", "Input extension after conversion.\n ex) `--after=jpg`") + flag.StringVar(&args.AfterExtension, "a", "png", "Input extension after conversion. (short)") + flag.Parse() + + osStat = os.Stat + osIsNotExist = os.IsNotExist + imgconv = imageconverter.Convert + +} + +func run() int { + if args.Directory == "" { + fmt.Fprintln(os.Stderr, "Input target Directory.\n ex) `--dir=./convert_image`") + return ExitCodeInvalidDirectoryError + } + + if _, err := osStat(args.Directory); osIsNotExist(err) { + fmt.Fprintln(os.Stderr, "Target directory is not found.") + return ExitCodeInvalidDirectoryError + } + + if err := imgconv(args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + return ExitCodeError + } + + return ExitCodeSuccess +} + +func main() { + os.Exit(run()) +} diff --git a/kadai2/wataboru/commands/imageconverter/main_test.go b/kadai2/wataboru/commands/imageconverter/main_test.go new file mode 100644 index 0000000..f12e796 --- /dev/null +++ b/kadai2/wataboru/commands/imageconverter/main_test.go @@ -0,0 +1,138 @@ +package main + +import ( + "flag" + "fmt" + "os" + "testing" + + "github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter" +) + +type mockFns struct { + osStat func(name string) (os.FileInfo, error) + osIsNotExist func(err error) bool + imgconv func(args imageconverter.Args) error +} + +func Test_run(t *testing.T) { + tests := []struct { + name string + args imageconverter.Args + mocks mockFns + want int + }{ + { + name: "Normal", + args: imageconverter.Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + mocks: mockFns{ + osStat: func(name string) (os.FileInfo, error) { + return nil, nil + }, + osIsNotExist: func(err error) bool { + return false + }, + imgconv: func(args imageconverter.Args) error { + return nil + }, + }, + want: ExitCodeSuccess, + }, + { + name: "ErrorBecauseWithoutArgsDirectory", + args: imageconverter.Args{ + Directory: "", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + mocks: mockFns{ + osStat: func(name string) (os.FileInfo, error) { + return nil, nil + }, + osIsNotExist: func(err error) bool { + return false + }, + imgconv: func(args imageconverter.Args) error { + return nil + }, + }, + want: ExitCodeInvalidDirectoryError, + }, + { + name: "ErrorBecauseOsIsNotExitReturnError", + args: imageconverter.Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + mocks: mockFns{ + osStat: func(name string) (os.FileInfo, error) { + return nil, nil + }, + osIsNotExist: func(err error) bool { + return true + }, + imgconv: func(args imageconverter.Args) error { + return nil + }, + }, + want: ExitCodeInvalidDirectoryError, + }, + { + name: "ErrorBecauseImgconvReturnError", + args: imageconverter.Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + mocks: mockFns{ + osStat: func(name string) (os.FileInfo, error) { + return nil, nil + }, + osIsNotExist: func(err error) bool { + return false + }, + imgconv: func(args imageconverter.Args) error { + return fmt.Errorf("error") + }, + }, + want: ExitCodeError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flagsSet(t, tt.args) + MockSet(t, tt.mocks) + if got := run(); got != tt.want { + t.Errorf("run() = %v, want %v", got, tt.want) + } + }) + } +} + +func flagsSet(t *testing.T, args imageconverter.Args) { + t.Helper() + if err := flag.CommandLine.Set("dir", args.Directory); err != nil { + t.Errorf("error occurred in flagSet helper: %v", err) + + } + if err := flag.CommandLine.Set("before", args.BeforeExtension); err != nil { + t.Errorf("error occurred in flagSet helper: %v", err) + + } + if err := flag.CommandLine.Set("after", args.AfterExtension); err != nil { + t.Errorf("error occurred in flagSet helper: %v", err) + + } +} + +func MockSet(t *testing.T, m mockFns) { + t.Helper() + osStat = m.osStat + osIsNotExist = m.osIsNotExist + imgconv = m.imgconv +} diff --git a/kadai2/wataboru/cover.out b/kadai2/wataboru/cover.out new file mode 100644 index 0000000..5fb30b7 --- /dev/null +++ b/kadai2/wataboru/cover.out @@ -0,0 +1,40 @@ +mode: set +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:29.13,33.46 4 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:36.2,37.44 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:40.2,41.33 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:33.46,35.3 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:37.44,39.3 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:57.65,59.2 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:63.64,65.2 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:69.64,71.2 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:75.64,77.2 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:81.65,83.2 1 0 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:85.52,86.45 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:87.23,88.28 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:89.14,90.27 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:91.14,92.27 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:93.14,94.27 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:95.23,96.28 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:97.10,98.106 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:102.44,104.16 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:107.2,110.16 3 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:114.2,114.15 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:120.2,121.16 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:125.2,126.16 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:129.2,129.32 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:104.16,106.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:110.16,112.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:114.15,115.17 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:115.17,117.4 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:121.16,123.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:126.16,128.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:132.78,133.16 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:137.2,137.18 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:141.2,142.37 2 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:145.2,145.72 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:133.16,135.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:137.18,139.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:142.37,144.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:151.31,152.92 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:152.92,154.3 1 1 +github.com/gopherdojo/dojo8/kadai1/wataboru/imageconverter/ImageConverter.go:157.48,159.2 1 1 diff --git a/kadai2/wataboru/go.mod b/kadai2/wataboru/go.mod new file mode 100644 index 0000000..3347058 --- /dev/null +++ b/kadai2/wataboru/go.mod @@ -0,0 +1,8 @@ +module github.com/gopherdojo/dojo8/kadai1/wataboru + +go 1.14 + +require ( + golang.org/x/image v0.0.0-20200618115811-c13761719519 + golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed // indirect +) diff --git a/kadai2/wataboru/go.sum b/kadai2/wataboru/go.sum new file mode 100644 index 0000000..55565af --- /dev/null +++ b/kadai2/wataboru/go.sum @@ -0,0 +1,30 @@ +github.com/yuin/goldmark v1.1.32 h1:5tjfNdR2ki3yYQ842+eX2sQHeiwpKJ0RnHO4IYOc4V8= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed h1:+qzWo37K31KxduIYaBeMqJ8MUOyTayOQKpH9aDPLMSY= +golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/kadai2/wataboru/imageconverter/ImageConverter.go b/kadai2/wataboru/imageconverter/ImageConverter.go new file mode 100644 index 0000000..8b43f62 --- /dev/null +++ b/kadai2/wataboru/imageconverter/ImageConverter.go @@ -0,0 +1,158 @@ +package imageconverter + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +var ( + convImage func(path string, info os.FileInfo, err error, args Args) error + execute func(source, dest string) (err error) + osOpen func(name string) (*os.File, error) + osCreate func(name string) (*os.File, error) + sourceFileClose func(file *os.File) error + destFileClose func(file *os.File) error + imageDecode func(r io.Reader) (image.Image, string, error) + newImgEncoder func(ext string) (Encoder, error) +) + +func init() { + convImage = convertImage + execute = exec + osOpen = os.Open + sourceFileClose = func(file *os.File) error { + return file.Close() + } + osCreate = os.Create + destFileClose = func(file *os.File) error { + return file.Close() + } + imageDecode = image.Decode + newImgEncoder = newImageEncoder +} + +type Args struct { + Directory string + BeforeExtension string + AfterExtension string +} + +type Encoder interface { + Encode(w io.Writer, m image.Image) error +} + +type JpegEncoder struct{} + +func (enc JpegEncoder) Encode(w io.Writer, m image.Image) error { + return jpeg.Encode(w, m, &jpeg.Options{Quality: jpeg.DefaultQuality}) +} + +type PngEncoder struct{} + +func (enc PngEncoder) Encode(w io.Writer, m image.Image) error { + return png.Encode(w, m) +} + +type GifEncoder struct{} + +func (enc GifEncoder) Encode(w io.Writer, m image.Image) error { + return gif.Encode(w, m, &gif.Options{NumColors: 256}) +} + +type BmpEncoder struct{} + +func (enc BmpEncoder) Encode(w io.Writer, m image.Image) error { + return bmp.Encode(w, m) +} + +type TiffEncoder struct{} + +func (enc TiffEncoder) Encode(w io.Writer, m image.Image) error { + return tiff.Encode(w, m, nil) +} + +func newImageEncoder(dest string) (Encoder, error) { + switch strings.ToLower(filepath.Ext(dest)) { + case ".jpg", ".jpeg": + return JpegEncoder{}, nil + case ".png": + return PngEncoder{}, nil + case ".gif": + return GifEncoder{}, nil + case ".bmp": + return BmpEncoder{}, nil + case ".tiff", ".tif": + return TiffEncoder{}, nil + default: + return nil, fmt.Errorf("image file could not be created due to an unknown extension. target: %s", dest) + } +} + +func exec(source, dest string) (err error) { + sourceFile, err := osOpen(source) + if err != nil { + return fmt.Errorf("file could not be opened. target: %s", source) + } + defer sourceFileClose(sourceFile) + + destFile, err := osCreate(dest) + if err != nil { + return fmt.Errorf("image file could not be created. target: %s", dest) + } + + defer func() { + if err == nil { + err = destFileClose(destFile) + } + }() + + img, _, err := imageDecode(sourceFile) + if err != nil { + return err + } + + e, err := newImgEncoder(dest) + if err != nil { + return err + } + return e.Encode(destFile, img) +} + +func convertImage(path string, info os.FileInfo, err error, args Args) error { + if err != nil { + return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v\n", path, err) + } + + if info.IsDir() { + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + if "."+args.BeforeExtension != ext { + return nil + } + return execute(path, replaceExt(info.Name(), "."+args.AfterExtension)) +} + +// 指定したディレクトリ以下のJPGファイルをPNGに変換(デフォルト) +// ディレクトリ以下は再帰的に処理する +// 変換前と変換後の画像形式を指定できる(オプション) +func Convert(args Args) error { + return filepath.Walk(args.Directory, func(path string, info os.FileInfo, err error) error { + return convImage(path, info, err, args) + }) +} + +func replaceExt(filePath, toExt string) string { + return filePath[:len(filePath)-len(filepath.Ext(filePath))] + toExt +} diff --git a/kadai2/wataboru/imageconverter/ImageConverter_test.go b/kadai2/wataboru/imageconverter/ImageConverter_test.go new file mode 100644 index 0000000..90a01f0 --- /dev/null +++ b/kadai2/wataboru/imageconverter/ImageConverter_test.go @@ -0,0 +1,509 @@ +package imageconverter + +import ( + "errors" + "fmt" + "image" + "io" + "os" + "reflect" + "testing" + "time" +) + +func Test_replaceExt(t *testing.T) { + type args struct { + filePath string + toExt string + } + tests := []struct { + name string + args args + want string + }{ + {"Normal", args{"hoge/fuga.jpg", ".png"}, "hoge/fuga.png"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := replaceExt(tt.args.filePath, tt.args.toExt); got != tt.want { + t.Errorf("replaceExt() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_Convert(t *testing.T) { + tests := []struct { + name string + args Args + convImage func(path string, info os.FileInfo, err error, args Args) error + wantErr bool + }{ + { + name: "Normal", + args: Args{"./hoge", "jpg", "png"}, + convImage: func(path string, info os.FileInfo, err error, args Args) error { + return nil + }, + wantErr: false, + }, + { + name: "ErrorBecauseWalkReturnError", + args: Args{"./hoge", "jpg", "png"}, + convImage: func(path string, info os.FileInfo, err error, args Args) error { + return fmt.Errorf("error") + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + convImage = tt.convImage + + if err := Convert(tt.args); (err != nil) != tt.wantErr { + t.Errorf("Convert() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_convertImage(t *testing.T) { + type args struct { + path string + info os.FileInfo + err error + args Args + } + tests := []struct { + name string + args args + e func(source, dest string) (err error) + wantErr bool + }{ + { + name: "Normal", + args: args{ + path: "./hoge/fuga.jpg", + info: FileInfoMock{}, + err: nil, + args: Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + }, + e: func(source, dest string) (err error) { + return nil + }, + wantErr: false, + }, + { + name: "NormalTargetIsDir", + args: args{ + path: "./hoge/fuga.jpg", + info: FileInfoIsDirTrueMock{}, + err: nil, + args: Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + }, + e: func(source, dest string) (err error) { + return nil + }, + wantErr: false, + }, + { + name: "NormalFileNameIsNotMatch", + args: args{ + path: "./hoge/fuga.jpg", + info: FileInfoMock{}, + err: nil, + args: Args{ + Directory: "./hoge", + BeforeExtension: "gif", + AfterExtension: "png", + }, + }, + e: func(source, dest string) (err error) { + return nil + }, + wantErr: false, + }, + { + name: "ErrorBecauseDirectoryError", + args: args{ + path: "./hoge/fuga.jpg", + info: FileInfoMock{}, + err: errors.New("error"), + args: Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + }, + e: func(source, dest string) (err error) { + return nil + }, + wantErr: true, + }, + { + name: "ErrorBecauseExecReturnError", + args: args{ + path: "./hoge/fuga.jpg", + info: FileInfoMock{}, + err: nil, + args: Args{ + Directory: "./hoge", + BeforeExtension: "jpg", + AfterExtension: "png", + }, + }, + e: func(source, dest string) (err error) { + return fmt.Errorf("error") + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execute = tt.e + if err := convertImage(tt.args.path, tt.args.info, tt.args.err, tt.args.args); (err != nil) != tt.wantErr { + t.Errorf("convertImage() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_exec(t *testing.T) { + type mockFns struct { + osOpen func(name string) (*os.File, error) + osCreate func(name string) (*os.File, error) + sourceFileClose func(file *os.File) error + destFileClose func(file *os.File) error + imageDecode func(r io.Reader) (image.Image, string, error) + newImgEncoder func(ext string) (Encoder, error) + } + type args struct { + source string + dest string + } + tests := []struct { + name string + args args + mockFns mockFns + wantErr bool + }{ + { + name: "Normal", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: false, + }, + { + name: "ErrorBecauseSourceFileOpenReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, fmt.Errorf("error") + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: true, + }, + { + name: "ErrorBecauseDestFileCreateReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, fmt.Errorf("error") + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: true, + }, + { + name: "ErrorBecauseSourceFileDecodeReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", fmt.Errorf("error") + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: true, + }, + { + name: "NormalBecauseNewImageEncoderReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return nil, fmt.Errorf("error") + }, + }, + wantErr: true, + }, + { + name: "NormalBecauseEncodeReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return nil + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return &EncoderErrorMock{}, nil + }, + }, + wantErr: true, + }, + { + name: "NormalBecauseDeferredSourceFileCloseReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return fmt.Errorf("error") + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return nil + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: false, + }, + { + name: "ErrorBecauseDeferredDestFileCloseReturnError", + args: args{source: "jpeg", dest: "png"}, + mockFns: mockFns{ + osOpen: func(name string) (*os.File, error) { + return nil, nil + }, + sourceFileClose: func(file *os.File) error { + return fmt.Errorf("error") + }, + osCreate: func(name string) (*os.File, error) { + return nil, nil + }, + destFileClose: func(file *os.File) error { + return fmt.Errorf("error") + }, + imageDecode: func(r io.Reader) (image.Image, string, error) { + return nil, "", nil + }, + newImgEncoder: func(ext string) (Encoder, error) { + return EncoderMock{}, nil + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osOpen = tt.mockFns.osOpen + sourceFileClose = tt.mockFns.sourceFileClose + osCreate = tt.mockFns.osCreate + destFileClose = tt.mockFns.destFileClose + imageDecode = tt.mockFns.imageDecode + newImgEncoder = tt.mockFns.newImgEncoder + if err := exec(tt.args.source, tt.args.dest); (err != nil) != tt.wantErr { + t.Errorf("exec() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_newImageEncoder(t *testing.T) { + type args struct { + dest string + } + tests := []struct { + name string + args args + want Encoder + wantErr bool + }{ + {name: "NormalJpeg", args: args{dest: "./hoge/fuga.jpeg"}, want: JpegEncoder{}, wantErr: false}, + {name: "NormalJpg", args: args{dest: "./hoge/fuga.jpg"}, want: JpegEncoder{}, wantErr: false}, + {name: "NormalPng", args: args{dest: "./hoge/fuga.png"}, want: PngEncoder{}, wantErr: false}, + {name: "NormalGif", args: args{dest: "./hoge/fuga.gif"}, want: GifEncoder{}, wantErr: false}, + {name: "NormalTiff", args: args{dest: "./hoge/fuga.tiff"}, want: TiffEncoder{}, wantErr: false}, + {name: "NormalTif", args: args{dest: "./hoge/fuga.tif"}, want: TiffEncoder{}, wantErr: false}, + {name: "NormalBmp", args: args{dest: "./hoge/fuga.bmp"}, want: BmpEncoder{}, wantErr: false}, + {name: "ErrorBecauseInvalidExtension", args: args{dest: "./hoge/fuga.pdf"}, want: nil, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newImageEncoder(tt.args.dest) + if (err != nil) != tt.wantErr { + t.Errorf("newImageEncoder() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("newImageEncoder() got = %v, want %v", got, tt.want) + } + }) + } +} + +type EncoderMock struct{} + +func (e EncoderMock) Encode(w io.Writer, m image.Image) error { + return nil +} + +type EncoderErrorMock struct{} + +func (e EncoderErrorMock) Encode(w io.Writer, m image.Image) error { + return fmt.Errorf("error") +} + +type FileInfoMock struct{} + +func (FileInfoMock) Name() string { + return "hoge.jpg" +} + +func (FileInfoMock) Size() int64 { + panic("implement me") +} + +func (FileInfoMock) Mode() os.FileMode { + panic("implement me") +} + +func (FileInfoMock) ModTime() time.Time { + panic("implement me") +} + +func (FileInfoMock) IsDir() bool { + return false +} + +func (FileInfoMock) Sys() interface{} { + panic("implement me") +} + +type FileInfoIsDirTrueMock struct{} + +func (FileInfoIsDirTrueMock) Name() string { + return "hoge.jpg" +} + +func (FileInfoIsDirTrueMock) Size() int64 { + panic("implement me") +} + +func (FileInfoIsDirTrueMock) Mode() os.FileMode { + panic("implement me") +} + +func (FileInfoIsDirTrueMock) ModTime() time.Time { + panic("implement me") +} + +func (FileInfoIsDirTrueMock) IsDir() bool { + return true +} + +func (FileInfoIsDirTrueMock) Sys() interface{} { + panic("implement me") +} diff --git a/kadai2/wataboru/imageconverter_coverage.html b/kadai2/wataboru/imageconverter_coverage.html new file mode 100644 index 0000000..3921369 --- /dev/null +++ b/kadai2/wataboru/imageconverter_coverage.html @@ -0,0 +1,261 @@ + + + + + + + + +
+ +
+ not tracked + + not covered + covered + +
+
+
+ + + +
+ + + diff --git a/kadai2/wataboru/main_coverage.html b/kadai2/wataboru/main_coverage.html new file mode 100644 index 0000000..5e743ab --- /dev/null +++ b/kadai2/wataboru/main_coverage.html @@ -0,0 +1,171 @@ + + + + + + + + +
+ +
+ not tracked + + not covered + covered + +
+
+
+ + + +
+ + + diff --git a/kadai2/wataboru/testdata/free-icon.jpg b/kadai2/wataboru/testdata/free-icon.jpg new file mode 100644 index 0000000..d49627e Binary files /dev/null and b/kadai2/wataboru/testdata/free-icon.jpg differ diff --git a/kadai2/wataboru/testdata/sub/free-icon-2.jpg b/kadai2/wataboru/testdata/sub/free-icon-2.jpg new file mode 100644 index 0000000..d49627e Binary files /dev/null and b/kadai2/wataboru/testdata/sub/free-icon-2.jpg differ