Merge branch '6-http-digital-clock' into 'master'
Resolve "http-digital-clock" Closes #6 See merge request slon/shad-go-private!6
114
digitalclock/README.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
## digitalclock
|
||||
|
||||
В этой задаче нужно написать http server,
|
||||
который по запросу `/?time=hh:mm:ss` вернет PNG с данным временем.
|
||||
|
||||
Сервер должен слушать порт, переданный через аргумент `--port`.
|
||||
|
||||
### Примеры
|
||||
|
||||
Запуск:
|
||||
```
|
||||
$ digitalclock -port 6029
|
||||
```
|
||||
|
||||
После запуска можно открыть в браузере `http://localhost:6029/?time=15:04:05` и получить png наподобие этой:
|
||||
|
||||
![Image description](assets/150405.png)
|
||||
|
||||
Если параметр `time` не задан (или равен ""), сервен должен вернуть текущее время.
|
||||
В этом случае при обновлении страницы картинка должна меняться.
|
||||
|
||||
В запросе также нужно поддержать параметр 1 <=`k` <= 30,
|
||||
который отвечает за увеличение картинки в `k` раз:
|
||||
|
||||
http://localhost:6029/?time=15:04:05?k=15
|
||||
|
||||
![Image description](assets/150405_15.png)
|
||||
|
||||
На невалидных `time` и `k` сервер должен возврщать произвольное сообщение об ошибке и http status 400 (Bad Request):
|
||||
|
||||
```
|
||||
$ curl -i "http://localhost:6029/?k=100"
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Fri, 14 Feb 2020 02:02:41 GMT
|
||||
Content-Length: 10
|
||||
|
||||
invalid k
|
||||
```
|
||||
|
||||
В случае успеха приложение должно возвращать http status 200 (OK),
|
||||
а также проставлять http header `Content-Type: image/png`:
|
||||
|
||||
```
|
||||
$ curl -i "http://localhost:6029/"
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: image/png
|
||||
Date: Fri, 14 Feb 2020 02:08:19 GMT
|
||||
Content-Length: 193
|
||||
...
|
||||
```
|
||||
|
||||
### Формат
|
||||
|
||||
`hh:mm:ss`
|
||||
|
||||
В [./symbols.go](./symbols.go) определены строковые константы, кодирующие цифры и двоеточие.
|
||||
Один символ константы (отличный от '\n') - 1 пиксель.
|
||||
'.' должны кодироваться в белый цвет, '1' - в бирюзовый (переменная **Cyan** в [./symbols.go](./symbols.go)).
|
||||
Все константы имеют одинаковую высоту `h`, все цифры имеют одинаковую ширину `w`.
|
||||
|
||||
`k` отличный от 1 (default) превращает каждый пиксель `p` картинки в блок `k x k` пикселей `p`.
|
||||
Высота результирующей картинки - `h * k`, ширина - `(6 * w + 2 * w_colon) * k`,
|
||||
где `w_colon` ширина константы, соответствующей ':'.
|
||||
|
||||
### Проверка решения
|
||||
|
||||
Для запуска тестов нужно выполнить следующую команду:
|
||||
|
||||
```
|
||||
go test -v ./digitalclock/...
|
||||
```
|
||||
|
||||
### Запуск программы
|
||||
|
||||
```
|
||||
go run -v ./digitalclock/...
|
||||
```
|
||||
|
||||
### Компиляция
|
||||
|
||||
```
|
||||
go install ./digitalclock/...
|
||||
```
|
||||
|
||||
После выполнения в `$GOPATH/bin` появится исполняемый файл с именем `digitalclock`.
|
||||
|
||||
### Walkthrough
|
||||
|
||||
1. парсинг аргументов командной строки https://gobyexample.com/command-line-flags
|
||||
2. http server: https://p.go.manytask.org/00-intro/lecture.slide#23
|
||||
3. url params: https://golang.org/pkg/net/url/#URL.Query
|
||||
3. форматирование времени: https://gobyexample.com/time-formatting-parsing
|
||||
4. работа с картинками: https://golang.org/pkg/image/
|
||||
|
||||
Пример, создания простой png размера 5x5:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 5, 5))
|
||||
img.Set(2, 2, color.RGBA{255, 0, 0, 255})
|
||||
f, _ := os.Create("/tmp/img.png")
|
||||
png.Encode(f, img)
|
||||
}
|
||||
```
|
BIN
digitalclock/assets/150405.png
Normal file
After Width: | Height: | Size: 196 B |
BIN
digitalclock/assets/150405_15.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
7
digitalclock/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
// +build !solution
|
||||
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
243
digitalclock/main_test.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/slon/shad-go/tools/testtool"
|
||||
)
|
||||
|
||||
const importPath = "gitlab.com/slon/shad-go/digitalclock"
|
||||
|
||||
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 startServer(t *testing.T) (port string, stop func()) {
|
||||
binary, err := binCache.GetBinary(importPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
port, err = testtool.GetFreePort()
|
||||
require.NoError(t, err, "unable to get free port")
|
||||
|
||||
cmd := exec.Command(binary, "-port", port)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
require.NoError(t, cmd.Start())
|
||||
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
stop = func() {
|
||||
_ = cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
|
||||
if err = testtool.WaitForPort(t, time.Second*30, port); err != nil {
|
||||
stop()
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
func readImage(fname string) (image.Image, error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open %s: %w", fname, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode %s: %w", fname, err)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func abs(a, b uint32) int64 {
|
||||
if a > b {
|
||||
return int64(a - b)
|
||||
}
|
||||
return int64(b - a)
|
||||
}
|
||||
|
||||
func calcImgDiff(i1, i2 image.Image) float64 {
|
||||
w, h := i1.Bounds().Dx(), i1.Bounds().Dy()
|
||||
|
||||
var sum int64
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
r1, g1, b1, _ := i1.At(x, y).RGBA()
|
||||
r2, g2, b2, _ := i2.At(x, y).RGBA()
|
||||
sum += abs(r1, r2)
|
||||
sum += abs(g1, g2)
|
||||
sum += abs(b1, b2)
|
||||
}
|
||||
}
|
||||
|
||||
return float64(sum) / (float64(w*h) * 0xffff * 3) * 100.0
|
||||
}
|
||||
|
||||
func queryImage(t *testing.T, c *http.Client, url string) image.Image {
|
||||
t.Logf("querying: %s", url)
|
||||
|
||||
resp, err := c.Get(url)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "image/png", resp.Header.Get("Content-Type"))
|
||||
|
||||
img, err := png.Decode(resp.Body)
|
||||
require.Nil(t, err)
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
func TestDigitalClock_valid(t *testing.T) {
|
||||
port, stop := startServer(t)
|
||||
defer stop()
|
||||
|
||||
files, err := ioutil.ReadDir("./testdata")
|
||||
require.Nil(t, err)
|
||||
|
||||
c := &http.Client{Timeout: time.Second * 10}
|
||||
|
||||
for _, f := range files {
|
||||
t.Run(f.Name(), func(t *testing.T) {
|
||||
parts := strings.SplitN(strings.TrimSuffix(f.Name(), ".png"), "_", 2)
|
||||
|
||||
v := url.Values{}
|
||||
v.Add("time", parts[0])
|
||||
v.Add("k", parts[1])
|
||||
|
||||
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
|
||||
img := queryImage(t, c, u)
|
||||
|
||||
expected, err := readImage(path.Join("testdata", f.Name()))
|
||||
require.NoError(t, err)
|
||||
|
||||
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
||||
ew, eh := expected.Bounds().Dx(), expected.Bounds().Dy()
|
||||
if w != ew || h != eh {
|
||||
t.Errorf("expected size %d x %d got %d x %d", ew, eh, w, h)
|
||||
}
|
||||
|
||||
diff := calcImgDiff(img, expected)
|
||||
t.Logf("image diff: %.2f %%", diff)
|
||||
require.True(t, diff < 1.0,
|
||||
fmt.Sprintf("%s images are too different (%.2f %%)", f.Name(), diff))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigitalClock_invalid(t *testing.T) {
|
||||
port, stop := startServer(t)
|
||||
defer stop()
|
||||
|
||||
c := &http.Client{Timeout: time.Second * 10}
|
||||
|
||||
type TestCase struct {
|
||||
Time string `json:"time"`
|
||||
K string `json:"k"`
|
||||
}
|
||||
|
||||
testName := func(tc *TestCase) string {
|
||||
data, err := json.Marshal(tc)
|
||||
require.Nil(t, err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
for _, tc := range []*TestCase{
|
||||
{K: "0"},
|
||||
{K: "31"},
|
||||
{K: "2.5"},
|
||||
{K: "f"},
|
||||
{Time: "15:04"},
|
||||
{Time: "15:04:0"},
|
||||
{Time: "3:04:05"},
|
||||
{Time: "15:4:05"},
|
||||
{Time: "24:00:00"},
|
||||
{Time: "00:00:60"},
|
||||
{Time: "f"},
|
||||
} {
|
||||
t.Run(testName(tc), func(t *testing.T) {
|
||||
v := url.Values{}
|
||||
if tc.Time != "" {
|
||||
v.Add("time", tc.Time)
|
||||
}
|
||||
if tc.K != "" {
|
||||
v.Add("k", tc.K)
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
|
||||
t.Logf("querying: %s", u)
|
||||
|
||||
resp, err := c.Get(u)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigitalClock_now(t *testing.T) {
|
||||
port, stop := startServer(t)
|
||||
defer stop()
|
||||
|
||||
c := &http.Client{Timeout: time.Second * 10}
|
||||
|
||||
for _, k := range []int{1, 10, 29} {
|
||||
k := k
|
||||
t.Run(fmt.Sprintf("k=%d", k), func(t *testing.T) {
|
||||
v := url.Values{"k": []string{strconv.Itoa(k)}}
|
||||
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
|
||||
img := queryImage(t, c, u)
|
||||
|
||||
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
||||
ew, eh := getExpectedWidth(k), getExpectedHeight(k)
|
||||
if w != ew || h != eh {
|
||||
t.Errorf("expected size %d x %d got %d x %d", ew, eh, w, h)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getSymbolWidth(s string) int {
|
||||
return len(strings.SplitN(s, "\n", 2)[0])
|
||||
}
|
||||
|
||||
func getExpectedWidth(k int) int {
|
||||
return (getSymbolWidth(Zero)*6 + getSymbolWidth(Colon)*2) * k
|
||||
}
|
||||
|
||||
func getExpectedHeight(k int) int {
|
||||
return len(strings.Split(Zero, "\n")) * k
|
||||
}
|
152
digitalclock/symbols.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
// +build !change
|
||||
|
||||
package main
|
||||
|
||||
import "image/color"
|
||||
|
||||
var Cyan = color.RGBA{R: 100, G: 200, B: 200, A: 0xff}
|
||||
|
||||
const (
|
||||
Zero = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
One = `........
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
........`
|
||||
|
||||
Two = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.111111.
|
||||
.111111.
|
||||
.11.....
|
||||
.11.....
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
Three = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
Four = `........
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
........`
|
||||
|
||||
Five = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.11.....
|
||||
.11.....
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
Six = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.11.....
|
||||
.11.....
|
||||
.111111.
|
||||
.111111.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
Seven = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
........`
|
||||
|
||||
Eight = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
........`
|
||||
|
||||
Nine = `........
|
||||
.111111.
|
||||
.111111.
|
||||
.11..11.
|
||||
.11..11.
|
||||
.111111.
|
||||
.111111.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
.....11.
|
||||
........`
|
||||
|
||||
Colon = `....
|
||||
....
|
||||
....
|
||||
....
|
||||
.11.
|
||||
.11.
|
||||
....
|
||||
.11.
|
||||
.11.
|
||||
....
|
||||
....
|
||||
....`
|
||||
)
|
BIN
digitalclock/testdata/00:51:41_20.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
digitalclock/testdata/02:06:08_11.png
vendored
Normal file
After Width: | Height: | Size: 763 B |
BIN
digitalclock/testdata/02:07:50_29.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
digitalclock/testdata/02:40:33_26.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
digitalclock/testdata/03:05:12_9.png
vendored
Normal file
After Width: | Height: | Size: 628 B |
BIN
digitalclock/testdata/03:55:20_19.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
digitalclock/testdata/03:57:01_9.png
vendored
Normal file
After Width: | Height: | Size: 615 B |
BIN
digitalclock/testdata/04:16:43_21.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
digitalclock/testdata/04:49:37_14.png
vendored
Normal file
After Width: | Height: | Size: 978 B |
BIN
digitalclock/testdata/05:01:26_14.png
vendored
Normal file
After Width: | Height: | Size: 995 B |
BIN
digitalclock/testdata/06:12:20_21.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
digitalclock/testdata/07:36:15_2.png
vendored
Normal file
After Width: | Height: | Size: 250 B |
BIN
digitalclock/testdata/09:00:51_21.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
digitalclock/testdata/09:17:08_23.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
digitalclock/testdata/11:14:07_30.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
digitalclock/testdata/13:10:10_13.png
vendored
Normal file
After Width: | Height: | Size: 896 B |
BIN
digitalclock/testdata/13:11:11_27.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
digitalclock/testdata/14:58:09_30.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
digitalclock/testdata/15:25:05_7.png
vendored
Normal file
After Width: | Height: | Size: 507 B |
BIN
digitalclock/testdata/15:37:28_14.png
vendored
Normal file
After Width: | Height: | Size: 996 B |
BIN
digitalclock/testdata/18:15:02_7.png
vendored
Normal file
After Width: | Height: | Size: 507 B |
BIN
digitalclock/testdata/19:03:33_7.png
vendored
Normal file
After Width: | Height: | Size: 506 B |
BIN
digitalclock/testdata/20:17:35_27.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
digitalclock/testdata/20:21:38_24.png
vendored
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
digitalclock/testdata/21:21:21_14.png
vendored
Normal file
After Width: | Height: | Size: 959 B |
BIN
digitalclock/testdata/22:14:54_27.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
digitalclock/testdata/22:16:10_20.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
digitalclock/testdata/22:48:18_10.png
vendored
Normal file
After Width: | Height: | Size: 711 B |
BIN
digitalclock/testdata/22:55:07_23.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
digitalclock/testdata/23:43:50_25.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |