diff --git a/kadai1/Udomomo/README.md b/kadai1/Udomomo/README.md new file mode 100644 index 0000000..009c307 --- /dev/null +++ b/kadai1/Udomomo/README.md @@ -0,0 +1,43 @@ +## 課題1 + * 次の仕様を満たすコマンドを作って下さい + - ディレクトリを指定する + - 指定したディレクトリ以下のJPGファイルをPNGに変換(デフォルト) + - ディレクトリ以下は再帰的に処理する + - 変換前と変換後の画像形式を指定できる(オプション) + + * 以下を満たすように開発してください + - mainパッケージと分離する + - 自作パッケージと標準パッケージと準標準パッケージのみ使う + - 準標準パッケージ:golang.org/x以下のパッケージ + - ユーザ定義型を作ってみる + - GoDocを生成してみる + + ## コマンド + * jpeg, png, jpg, gifに対応 + + ## インストール + ``` + $ go get github.com/Udomomo/dojo4/kadai1/Udomomo/convimg + $ cd $GOPATH/src/github.com/Udomomo/dojo4/kadai1/Udomomo/convimg + $ git checkout kadai1-Udomomo + $ git fetch && git merge + $ git install +``` + +## ビルド + ``` + $ go build -o bin/convimg main.go + ``` + + ## コマンド + ``` + $./bin/convimg [options] [directories] + ``` + + ### オプション + ``` +-f string + format before conversion (default "jpg") +-t string + format after conversion (default "png") + ``` \ No newline at end of file diff --git a/kadai1/Udomomo/convimg/converter.go b/kadai1/Udomomo/convimg/converter.go new file mode 100644 index 0000000..8dd6623 --- /dev/null +++ b/kadai1/Udomomo/convimg/converter.go @@ -0,0 +1,45 @@ +package convimg + +import ( + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" +) + +type Converter interface { + convimg() error +} + +type jpgConverter struct { + dist io.Writer + img image.Image +} + +type pngConverter struct { + dist io.Writer + img image.Image +} + +type gifConverter struct { + dist io.Writer + img image.Image +} + +//convert : ファイルの形式に合わせて、変換を行うメソッドを呼び分ける +func convert(c Converter) error { + return c.convimg() +} + +func (c jpgConverter) convimg() error { + return jpeg.Encode(c.dist, c.img, nil) +} + +func (c pngConverter) convimg() error { + return png.Encode(c.dist, c.img) +} + +func (c gifConverter) convimg() error { + return gif.Encode(c.dist, c.img, nil) +} diff --git a/kadai1/Udomomo/convimg/convimg.go b/kadai1/Udomomo/convimg/convimg.go new file mode 100644 index 0000000..92f0087 --- /dev/null +++ b/kadai1/Udomomo/convimg/convimg.go @@ -0,0 +1,123 @@ +package convimg + +import ( + "fmt" + "image" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +//SearchFile : searchFile関数が出力する変換後のパスを貯めておき、返り値として返す。 +func SearchFile(rootDir, from, to string) []string { + processingPaths := make([]string, 0) + processingPaths = searchFile(rootDir, from, to, processingPaths) + + processedPaths := make([]string, 0) + for _, p := range processingPaths { + e := filepath.Ext(p) + np := p[:len(p)-len(e)] + to + + pp, err := convFile(p, np, to) + if err != nil { + log.Fatal(err) + } + processedPaths = append(processedPaths, pp) + os.Remove(p) + } + + //os.Remove後の結果はユニットテストしづらいので、ここで簡易的に確認する + if len(processingPaths) != len(processedPaths) { + log.Fatal("len of conv results is wrong") + } + return processedPaths +} + +//searchFile : rootDirにあるファイルの一覧を探索。ディレクトリがあれば再帰処理する。 +func searchFile(rootDir, from, to string, processingPaths []string) []string { + + files, err := ioutil.ReadDir(rootDir) + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + path := filepath.Join(rootDir, file.Name()) + if file.IsDir() { + processingPaths = searchFile(path, from, to, processingPaths) + continue + } + + willConv := validateIfConvNeeded(path, from, to) + if willConv == false { + continue + } + + processingPaths = append(processingPaths, path) + } + return processingPaths +} + +//generateNewExt : 変換が必要な場合、変換後のパスを生成して返す +func validateIfConvNeeded(path, from, to string) bool { + ext := filepath.Ext(path) + + //変換したい拡張子でなければ何もしない + if ext != from { + return false + } + + //変換する必要がなければ何もしない + if from == to { + return false + } + + return true +} + +//convFile : 変換を実行する +func convFile(path, newPath, toExt string) (string, error) { + file, err := os.Open(path) + if err != nil { + return newPath, fmt.Errorf("Original file open failed. Path: %s", path) + } + defer file.Close() + + decoded, _, err := image.Decode(file) + if err != nil { + return newPath, fmt.Errorf("Original file decode failed. Path: %s", path) + } + + out, err := os.Create(newPath) + if err != nil { + return newPath, fmt.Errorf("New empty file creation failed. Path: %s", newPath) + } + defer out.Close() + + var c Converter + switch toExt { + case ".jpeg", ".jpg": + { + c = &jpgConverter{out, decoded} + } + case ".png": + { + c = &pngConverter{out, decoded} + } + case ".gif": + { + c = &gifConverter{out, decoded} + } + default: + { + return newPath, fmt.Errorf("Can't convert to illegal format: %s", toExt) + } + } + + if err := c.convimg(); err != nil { + return newPath, fmt.Errorf("Encode failed from %s to %s", path, newPath) + } + + return newPath, nil +} diff --git a/kadai1/Udomomo/convimg/convimg_test.go b/kadai1/Udomomo/convimg/convimg_test.go new file mode 100644 index 0000000..6a30135 --- /dev/null +++ b/kadai1/Udomomo/convimg/convimg_test.go @@ -0,0 +1,144 @@ +package convimg + +import ( + "image" + "os" + "testing" +) + +func TestSearchFile(t *testing.T) { + t.Helper() + expProcessingPath := []string{ + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.jpg", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test2/test2.jpg", + } + + processingPath := searchFile("/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata", ".jpg", ".png", make([]string, 0)) + for _, p := range expProcessingPath { + if contains(processingPath, p) == false { + t.Errorf("searchFile failed: %s is not searched", p) + } + } + + if len(processingPath) != len(expProcessingPath) { + t.Errorf("searchFile failed: result is longer than expected: %v", processingPath) + } +} + +func contains(su []string, fl string) bool { + for _, s := range su { + if s == fl { + return true + } + } + return false +} + +func TestValidateIfConvNeeded(t *testing.T) { + t.Helper() + jpgPath := "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.jpg" + if validateIfConvNeeded(jpgPath, ".jpg", ".gif") == false { + t.Error("validate failed: file should be converted") + } + if validateIfConvNeeded(jpgPath, ".png", ".gif") == true { + t.Error("validate failed: fromExt difference is ignored") + } + if validateIfConvNeeded(jpgPath, ".jpg", ".jpg") == true { + t.Error("validate failed: from and to are equal") + } +} + +func TestConvFile(t *testing.T) { + t.Helper() + var testCase = []struct { + path string + newPath string + toExt string + expFmt string + wantErr bool + }{ + //正常ケース png->jpg + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.png", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.jpg", + ".jpg", + "jpeg", + false, + }, + //正常ケース jpg->gif + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.jpg", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.gif", + ".gif", + "gif", + false, + }, + //正常ケース gif->png + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.gif", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.png", + ".png", + "png", + false, + }, + //異常ケース 存在しないファイルパスの場合 + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/hogehoge.gif", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.png", + ".png", + "png", + true, + }, + //異常ケース ファイルが壊れている場合 + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/broken.gif", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.png", + ".png", + "png", + true, + }, + //異常ケース 書き込み先の権限がない場合 + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.gif", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/permission/test.png", + ".png", + "png", + true, + }, + //異常ケース 変換先の拡張子が不正な場合 + {"/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/testdata/test.gif", + "/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/test.pmg", + ".pmg", + "png", + true, + }, + } + + if err := os.Mkdir("/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output", 0755); err != nil { + t.Fatal(err) + } + defer os.RemoveAll("/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output") + + //書き込み先に権限がない場合のテスト用にディレクトリを作っておく + if err := os.Mkdir("/Users/naoya/Golang_practice/dojo4/kadai1/Udomomo/output/permission", 0555); err != nil { + t.Fatal(err) + } + + for _, tc := range testCase { + processedPath, err := convFile(tc.path, tc.newPath, tc.toExt) + if err != nil && tc.wantErr == false { + t.Errorf("Case %s should succeed but returned error: %#v", tc.path, err) + } + + if err == nil && tc.wantErr == true { + t.Errorf("Case %s should fail but succeeded", tc.path) + } + + if err == nil { + file, err := os.Open(processedPath) + if err != nil { + t.Errorf("processed file can't open. Path: %s", processedPath) + } + defer file.Close() + + if _, fmt, _ := image.Decode(file); fmt != tc.expFmt { + t.Errorf("fmt is wrong: expected %s but actually %s", tc.expFmt, fmt) + } + } + } + +} diff --git a/kadai1/Udomomo/main.go b/kadai1/Udomomo/main.go new file mode 100644 index 0000000..588573c --- /dev/null +++ b/kadai1/Udomomo/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/gopherdojo/dojo4/kadai1/Udomomo/convimg" +) + +func main() { + + SUFFIX := []string{"jpg", "jpeg", "png", "gif"} + SEPARATOR := "." + + var ( + f string + t string + ) + + flag.StringVar(&f, "f", "jpg", "format before conversion") + flag.StringVar(&t, "t", "png", "format after conversion") + + flag.Parse() + + path := flag.Arg(0) + + if contains(SUFFIX, f) == false || contains(SUFFIX, t) == false { + fmt.Printf("Invalid suffix: %s, %s\n", f, t) + os.Exit(1) + } + + processedPaths := convimg.SearchFile(path, SEPARATOR+f, SEPARATOR+t) + for _, p := range processedPaths { + println(p) + } +} + +func contains(su []string, fl string) bool { + for _, s := range su { + if s == fl { + return true + } + } + return false +} diff --git a/kadai1/Udomomo/testdata/broken.gif b/kadai1/Udomomo/testdata/broken.gif new file mode 100644 index 0000000..4a4da00 Binary files /dev/null and b/kadai1/Udomomo/testdata/broken.gif differ diff --git a/kadai1/Udomomo/testdata/test.gif b/kadai1/Udomomo/testdata/test.gif new file mode 100644 index 0000000..6c85951 Binary files /dev/null and b/kadai1/Udomomo/testdata/test.gif differ diff --git a/kadai1/Udomomo/testdata/test.jpg b/kadai1/Udomomo/testdata/test.jpg new file mode 100644 index 0000000..dc98d75 Binary files /dev/null and b/kadai1/Udomomo/testdata/test.jpg differ diff --git a/kadai1/Udomomo/testdata/test.png b/kadai1/Udomomo/testdata/test.png new file mode 100644 index 0000000..f90edd0 Binary files /dev/null and b/kadai1/Udomomo/testdata/test.png differ diff --git a/kadai1/Udomomo/testdata/test2/test2.jpg b/kadai1/Udomomo/testdata/test2/test2.jpg new file mode 100644 index 0000000..dc98d75 Binary files /dev/null and b/kadai1/Udomomo/testdata/test2/test2.jpg differ diff --git a/kadai2/README.md b/kadai2/README.md new file mode 100644 index 0000000..fc22d5b --- /dev/null +++ b/kadai2/README.md @@ -0,0 +1,42 @@ +# 課題2-1 io.Readerとio.Writerについて +## 何に使われているか +使われているパッケージは多い。一例として以下のようなものがある。 +- os.File +- strings.NewReader +- http.Request.Body +- bytes.Buffer +- ioutil.ReadAll + +これらのメソッドで、引数として使われていることが多い。 + +## どのような利点があるのか +最大の特長は、対象を限定せずに「読む/書く」という処理を抽象化したことにある。文字列だけではなく、ファイルやhttpリクエストもio.Reader・io.Writerで処理することができる。 +こうすることで、読み書きのみを責務として担う関数を使いまわすことができる。Golangのinterfaceは型の一種なので、例えば後ろから一定の文字数だけ読むtailという関数があった場合、引数をio.Readerにすれば、文字列もファイルも同じtail関数で読めるようになる。テストするときも、読み出し部分のテストは1つのメソッドについて行えばよい。 +また、より効率の良い読み方を求めて新しいnewTail関数を作ったとしても、その関数以外の部分に手を加える必要がなく、リファクタリングが最小限で済む。 +このように、interfaceを上手に定義しておくことで、読む/書くための関数とそれ以外の関数をより疎結合に近い状態にしやすくなる。 + +上記はinterface一般にいえるメリットだが、io.Reader・io.Writerならではの良い点は、interfaceの中に定義されている関数が1つしかなく、それでいて意味的にも関数1つで過不足がないことにある。よけいな関数を定義しすぎてinterfaceが肥大化すると、後々そのinterfaceを使った新しいstructを定義するときに、必要性の薄い関数も改めて書かなければならず足を引っ張ることになりやすい。io.Reader・io.Writerは、Read/Write関数を定義しさえすれば使えるので、コードを書く側にとってもほとんど邪魔にならない。 + +# 課題2-2 +## リファクタリング +- 画像変換の入り口のメソッドであるsearchFile関数に返り値を持たせた。 + - もともとは返り値を持たず、変換を実行するconvFile関数の返り値(=新しいパス)を最後にprintlnするだけだったが、新しいパス自体を返り値とする方が、searchFile関数の結果を取得してテストする処理を書きやすい気がした。 + +- 変換用関数convFileをテストしやすくするために、os.Removeを変換用の関数から外に出した(元ファイルが消えてしまうので非常にやりにくい) + +- ファイルの再帰的探索と変換処理とを分離させた。これまでは一つファイルを見つけてはその場で変換をしていたが、変換対象のファイルを全部見つけた後にまとめて変換することで、探索と変換の両方をテストしやすくした。 + +- 関数内でエラー時にlog.Fatalですぐ終了しまうのをやめ、errorを返り値として返し、呼び出し元でエラーの中身を見て終了するようにした + - すると、関数の戻り値としてエラーを受け取ってテストできる。どのエラーになったかもテストしやすい。 + +## ヘルパー関数 +- 各テスト関数に設置した。 + +## テーブル駆動テスト +- convFileのテストで使用している。 + +## カバレッジ +``` +go test -coverprofile=cover.out +go tool cover -html=cover.out -o cover.html +``` \ No newline at end of file