diff --git a/fetchall/README.md b/fetchall/README.md new file mode 100644 index 0000000..a57df4c --- /dev/null +++ b/fetchall/README.md @@ -0,0 +1,50 @@ +## fetchall + +В этой задаче нужно написать консольную утилиту, +которая принимает на вход произвольное количество http URL'ов и скачивает их содержимое **конкурентно**. + +Программа не должна останавливаться на невалидном URL'e. +Текст ответов можно игнорировать. +Вместо этого можно залогировать прогресс в произвольном формате. + +Пример: +``` +fetchall/solution.go https://gopl.io golang.org http://golang.org +Get golang.org: unsupported protocol scheme "" +1.05s 11071 http://golang.org +2.18s 4154 https://gopl.io +2.18s elapsed +``` + +В примере логируются времена обработки индивидуальных запросов, размеры ответов и общее время работы программы. +Можно видеть, что общее время работы равно максимуму, а не сумме времён индивидуальных запросов. + +### Проверка решения + +Для запуска тестов нужно выполнить следующую команду: + +``` +go test -v ./fetchall/... +``` + +### Запуск программы + +``` +go run -v ./fetchall/main.go +``` + +### Компиляция + +``` +go install ./fetchall/... +``` + +После выполнения в `$GOPATH/bin` появится исполняемый файл с именем `fetchall`. + +### Ссылки + +1. Чтение аргументов командной строки: https://gobyexample.com/command-line-arguments +2. HTTP запрос: https://gobyexample.com/http-clients +3. Запуск горутин: https://gobyexample.com/goroutines +4. Ожидание завершения горутин: https://gobyexample.com/channels +4. Замер времени: https://golang.org/pkg/time/#Since diff --git a/fetchall/main.go b/fetchall/main.go new file mode 100644 index 0000000..cab7d3a --- /dev/null +++ b/fetchall/main.go @@ -0,0 +1,7 @@ +// +build !solution + +package main + +func main() { + +} diff --git a/fetchall/main_test.go b/fetchall/main_test.go new file mode 100644 index 0000000..3201f6d --- /dev/null +++ b/fetchall/main_test.go @@ -0,0 +1,162 @@ +// +build !change + +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "sort" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/tools/testtool" +) + +const fetchallImportPath = "gitlab.com/slon/shad-go/fetchall" + +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 TestFetchall_valid(t *testing.T) { + binary, err := binCache.GetBinary(fetchallImportPath) + require.Nil(t, err) + + type endpoint string + + for _, tc := range []struct { + name string + h http.HandlerFunc + queries []endpoint + }{ + { + 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())}, + }, + { + 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{"/"}, + }, + { + 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"}, + }, + } { + 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 + + require.Nil(t, cmd.Run()) + }) + } +} + +func TestFetchall_malformed(t *testing.T) { + binary, err := binCache.GetBinary(fetchallImportPath) + require.Nil(t, err) + + hit := int32(0) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&hit, 1) + _, _ = w.Write([]byte("success")) + })) + defer s.Close() + + cmd := exec.Command(binary, "golang.org", s.URL, s.URL) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + err = cmd.Run() + require.Nil(t, err) + + require.True(t, atomic.LoadInt32(&hit) >= 2) +} + +func TestFetchall_concurrency(t *testing.T) { + binary, err := binCache.GetBinary(fetchallImportPath) + require.Nil(t, err) + + var mu sync.Mutex + var callOrder []time.Duration + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s := r.URL.Query().Get("duration") + require.NotEmpty(t, s) + + d, err := time.ParseDuration(s) + require.Nil(t, err) + + time.Sleep(d) + + mu.Lock() + callOrder = append(callOrder, d) + mu.Unlock() + + _, _ = fmt.Fprintln(w, "hello") + })) + defer s.Close() + + makeURL := func(d time.Duration) string { + v := url.Values{} + v.Add("duration", d.String()) + return fmt.Sprintf("%s?%s", s.URL, v.Encode()) + } + + fastURL := makeURL(time.Millisecond * 10) + slowURL := makeURL(time.Second) + + cmd := exec.Command(binary, slowURL, fastURL) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + + require.Nil(t, cmd.Run()) + + mu.Lock() + defer mu.Unlock() + + require.Len(t, callOrder, 2) + require.True(t, sort.SliceIsSorted(callOrder, func(i, j int) bool { + return callOrder[i] < callOrder[j] + })) +}