From df4a9d56c3fa00ccf110edea346515eb19db28cd Mon Sep 17 00:00:00 2001 From: verytable Date: Thu, 13 Feb 2020 19:54:24 +0000 Subject: [PATCH] Resolve "url-fetch" --- .deadlines.yml | 2 + urlfetch/README.md | 51 ++++++++++++ urlfetch/main.go | 7 ++ urlfetch/main_test.go | 184 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 urlfetch/README.md create mode 100644 urlfetch/main.go create mode 100644 urlfetch/main_test.go diff --git a/.deadlines.yml b/.deadlines.yml index a259e5a..654f0c3 100644 --- a/.deadlines.yml +++ b/.deadlines.yml @@ -8,3 +8,5 @@ score: 100 - task: wordcount score: 100 + - task: urlfetch + score: 100 diff --git a/urlfetch/README.md b/urlfetch/README.md new file mode 100644 index 0000000..04746fd --- /dev/null +++ b/urlfetch/README.md @@ -0,0 +1,51 @@ +## urlfetch + +В этой задаче нужно написать консольную утилиту, +которая принимает на вход произвольное количество http URL'ов и последоватльно скачивает их содержимое. + +При обработке невалидного URL'а программа должна завершаться с ненулевым exit кодом. + +### Примеры + +Успешный запуск: +``` +$ urlfetch https://golang.org https://go.dev + + + + +... +``` + +Неуспешный запуск: +``` +$ urlfetch golang.org +fetch: Get golang.org: unsupported protocol scheme "" +``` + +### Проверка решения + +Для запуска тестов нужно выполнить следующую команду: + +``` +go test -v ./urlfetch/... +``` + +### Запуск программы + +``` +go run -v ./urlfetch/main.go +``` + +### Компиляция + +``` +go install ./urlfetch/... +``` + +После выполнения в `$GOPATH/bin` появится исполняемый файл с именем `urlfetch`. + +### Walkthrough + +1. Чтение аргументов командной строки: https://gobyexample.com/command-line-arguments +2. HTTP запрос: https://gobyexample.com/http-clients diff --git a/urlfetch/main.go b/urlfetch/main.go new file mode 100644 index 0000000..cab7d3a --- /dev/null +++ b/urlfetch/main.go @@ -0,0 +1,7 @@ +// +build !solution + +package main + +func main() { + +} diff --git a/urlfetch/main_test.go b/urlfetch/main_test.go new file mode 100644 index 0000000..99b441d --- /dev/null +++ b/urlfetch/main_test.go @@ -0,0 +1,184 @@ +// +build !change + +package main + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strconv" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/tools/testtool" +) + +const urlfetchImportPath = "gitlab.com/slon/shad-go/urlfetch" + +var binCache testtool.BinCache + +func TestMain(m *testing.M) { + os.Exit(func() int { + var teardown testtool.CloseFunc + binCache, teardown = testtool.NewBinCache() + defer teardown() + + return m.Run() + }()) +} + +func TestUrlFetch_valid(t *testing.T) { + binary, err := binCache.GetBinary(urlfetchImportPath) + require.Nil(t, err) + + type endpoint string + type response string + + for _, tc := range []struct { + name string + h http.HandlerFunc + queries []endpoint + expected []response + }{ + { + name: "404", + h: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "The requested URL was not found.", http.StatusNotFound) + }, + queries: []endpoint{"/" + endpoint(testtool.RandomName())}, + expected: []response{"The requested URL was not found."}, + }, + { + name: "200", + h: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("The requested URL was found.\n")) + }, + queries: []endpoint{"/"}, + expected: []response{"The requested URL was found.\n"}, + }, + { + name: "multiple-urls", + h: func(w http.ResponseWriter, r *http.Request) { + mux := http.NewServeMux() + mux.HandleFunc("/foo", func(w http.ResponseWriter, h *http.Request) { + _, _ = w.Write([]byte("foo")) + }) + mux.HandleFunc("/bar", func(w http.ResponseWriter, h *http.Request) { + _, _ = w.Write([]byte("bar")) + }) + mux.ServeHTTP(w, r) + }, + queries: []endpoint{"/foo", "/bar"}, + expected: []response{"foo", "bar"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + s := httptest.NewServer(tc.h) + defer s.Close() + + urls := make([]string, len(tc.queries)) + for i, q := range tc.queries { + urls[i] = s.URL + string(q) + } + + cmd := exec.Command(binary, urls...) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + data, err := cmd.Output() + require.Nil(t, err) + + for _, r := range tc.expected { + require.True(t, bytes.Contains(data, []byte(r)), + fmt.Sprintf(`output="%s" does not contain expected response="%s"`, data, r)) + } + + }) + } +} + +func TestUrlFetch_malformed(t *testing.T) { + binary, err := binCache.GetBinary(urlfetchImportPath) + require.Nil(t, err) + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("success")) + })) + defer s.Close() + + for _, tc := range []struct { + name string + urls []string + }{ + { + name: "invalid-protocol-scheme", + urls: []string{"golang.org"}, + }, + { + name: "valid+invalid", + urls: []string{s.URL, "golang.org"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + cmd := exec.Command(binary, tc.urls...) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + err = cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + + t.Fatalf("process ran with err=%v, want exit status != 0", err) + }) + } +} + +func TestUrlFetch_order(t *testing.T) { + binary, err := binCache.GetBinary(urlfetchImportPath) + require.Nil(t, err) + + mux := http.NewServeMux() + + var expectedCallOrder []int + + var mu sync.Mutex + var callOrder []int + + n := 1000 + for i := 0; i < n; i++ { + i := i + expectedCallOrder = append(expectedCallOrder, i) + s := strconv.Itoa(i) + mux.HandleFunc("/"+s, func(w http.ResponseWriter, h *http.Request) { + mu.Lock() + callOrder = append(callOrder, i) + mu.Unlock() + _, _ = w.Write([]byte(s)) + }) + } + + s := httptest.NewServer(mux) + defer s.Close() + + urls := make([]string, n) + for i := 0; i < n; i++ { + urls[i] = s.URL + "/" + strconv.Itoa(i) + } + + cmd := exec.Command(binary, urls...) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + require.Nil(t, cmd.Run()) + + mu.Lock() + defer mu.Unlock() + + require.Equal(t, expectedCallOrder, callOrder) +}