Repository: isucon/isucon11-qualify (language: go)

Issue:
[bench] graphの検証



Discussion / hints:

Fix patch:
diff --git a/bench/model/graph.go b/bench/model/graph.go
new file mode 100644
index 000000000..6509a73f1
--- /dev/null
+++ b/bench/model/graph.go
@@ -0,0 +1,103 @@
+package model
+
+const (
+	// graph の1要素 (1h) を構成するコンディション数のボーダー値
+	// この値を下回った分だけ減点処理される
+	missingDataBorder = 50
+)
+
+var (
+	scorePerCondition = map[string]int{
+		"is_dirty":      -1,
+		"is_overweight": -1,
+		"is_broken":     -5,
+	}
+)
+
+// Graph Model は verifyGraph にて利用される。
+// getGraph のレスポンスボディのうち condition_timestamps を元にモデルを組み立て、
+// モデルを用いて getGraph のレスポンスボディの他フィールドが適切な値かどうかを検証する。
+type Graph struct {
+	conditions []*IsuCondition
+
+	score   int
+	sitting int
+	detail  GraphDetail
+}
+
+type GraphDetail struct {
+	isBroken     int
+	isDirty      int
+	isOverweight int
+	missingData  int
+}
+
+func NewGraph(c []*IsuCondition) Graph {
+	graph := Graph{conditions: c}
+	graph.calculate()
+	return graph
+}
+
+// g.condition を元に他フィールドを埋めるメソッド
+func (g *Graph) calculate() {
+	sittingCount := 0
+	for _, log := range g.conditions {
+		if log.IsSitting {
+			sittingCount++
+		}
+	}
+	if sittingCount != 0 {
+		g.sitting = sittingCount * 100 / len(g.conditions)
+	} else {
+		g.sitting = 0
+	}
+
+	//score&detail
+	g.score = 100
+	//condition要因の減点
+	for _, c := range g.conditions {
+		//trueになっているものは減点
+		if c.IsDirty {
+			g.score += scorePerCondition["is_dirty"]
+			g.detail.isDirty += scorePerCondition["is_dirty"]
+		}
+		if c.IsOverweight {
+			g.score += scorePerCondition["is_overweight"]
+			g.detail.isOverweight += scorePerCondition["is_overweight"]
+		}
+		if c.IsBroken {
+			g.score += scorePerCondition["is_broken"]
+			g.detail.isBroken += scorePerCondition["is_broken"]
+		}
+	}
+	//個数減点
+	if len(g.conditions) < missingDataBorder {
+		minus := -(missingDataBorder - len(g.conditions)) * 2
+		g.score += minus
+		g.detail.missingData = minus
+	}
+	if g.score < 0 {
+		g.score = 0
+	}
+}
+
+// getters
+
+func (g Graph) Score() int {
+	return g.score
+}
+func (g Graph) Sitting() int {
+	return g.sitting
+}
+func (g Graph) IsBroken() int {
+	return g.detail.isBroken
+}
+func (g Graph) IsDirty() int {
+	return g.detail.isDirty
+}
+func (g Graph) IsOverweight() int {
+	return g.detail.isOverweight
+}
+func (g Graph) MissingData() int {
+	return g.detail.missingData
+}
diff --git a/bench/scenario/action.go b/bench/scenario/action.go
index 4ed90ab27..75b2319b1 100644
--- a/bench/scenario/action.go
+++ b/bench/scenario/action.go
@@ -572,15 +572,19 @@ func getConditionRequestParams(base string, req service.GetIsuConditionRequest)
 	return targetURL.String()
 }
 
-func getIsuGraphAction(ctx context.Context, a *agent.Agent, id string, date int64) ([]*service.GraphResponse, *http.Response, error) {
-	graph := []*service.GraphResponse{}
-	reqUrl := fmt.Sprintf("/api/isu/%s/graph?date=%d", id, date)
+func getIsuGraphAction(ctx context.Context, a *agent.Agent, id string, req service.GetGraphRequest) (service.GraphResponse, *http.Response, error) {
+	graph := service.GraphResponse{}
+	reqUrl := fmt.Sprintf("/api/isu/%s/graph?date=%d", id, req.Date)
 	res, err := reqJSONResJSON(ctx, a, http.MethodGet, reqUrl, nil, &graph, []int{http.StatusOK})
 	if err != nil {
 		return nil, nil, err
 	}
 
 	//TODO: バリデーション
+	// res, text, err := reqJSONResError(ctx, a, http.MethodPost, reqUrl, bytes.NewReader(body), []int{http.StatusNotFound, http.StatusBadRequest})
+	// if err != nil {
+	// 	return "", nil, err
+	// }
 
 	return graph, res, nil
 }
@@ -697,8 +701,8 @@ func browserGetIsuConditionAction(ctx context.Context, a *agent.Agent, id string
 }
 
 func browserGetIsuGraphAction(ctx context.Context, a *agent.Agent, id string, date int64,
-	validateGraph func(*http.Response, []*service.GraphResponse) []error,
-) (*service.Isu, []*service.GraphResponse, []error) {
+	validateGraph func(*http.Response, service.GraphResponse) []error,
+) (*service.Isu, service.GraphResponse, []error) {
 	// TODO: 静的ファイルのGET
 
 	errors := []error{}
@@ -707,7 +711,8 @@ func browserGetIsuGraphAction(ctx context.Context, a *agent.Agent, id string, da
 	if err != nil {
 		errors = append(errors, err)
 	}
-	graph, res, err := getIsuGraphAction(ctx, a, id, date)
+	req := service.GetGraphRequest{Date: date}
+	graph, res, err := getIsuGraphAction(ctx, a, id, req)
 	if err != nil {
 		errors = append(errors, err)
 	} else {
diff --git a/bench/scenario/load.go b/bench/scenario/load.go
index 883254134..6f1898cd8 100644
--- a/bench/scenario/load.go
+++ b/bench/scenario/load.go
@@ -211,10 +211,10 @@ func (s *Scenario) requestNewConditionScenario(ctx context.Context, step *isucan
 	// 最新の condition から、一度見た condition が帰ってくるまで condition のページングをする
 	nowVirtualTime := s.ToVirtualTime(time.Now())
 	request := service.GetIndividualIsuConditionRequest{
-		StartTime:        nil,
-		CursorEndTime:    nowVirtualTime.Unix(),
-		ConditionLevel:   "info,warning,critical",
-		Limit:            nil,
+		StartTime:      nil,
+		CursorEndTime:  nowVirtualTime.Unix(),
+		ConditionLevel: "info,warning,critical",
+		Limit:          nil,
 	}
 	conditions, errs := s.getIsuConditionUntilAlreadyRead(ctx, user, targetIsu, request)
 	if len(errs) != 0 {
@@ -223,7 +223,7 @@ func (s *Scenario) requestNewConditionScenario(ctx context.Context, step *isucan
 		}
 		return
 	}
-	if len(conditions) == 0  {
+	if len(conditions) == 0 {
 		return
 	}
 
@@ -254,10 +254,10 @@ func (s *Scenario) requestLastBadConditionScenario(ctx context.Context, step *is
 	// ConditionLevel最新の condition から、一度見た condition が帰ってくるまで condition のページングをする
 	nowVirtualTime := s.ToVirtualTime(time.Now())
 	request := service.GetIndividualIsuConditionRequest{
-		StartTime:        nil,
-		CursorEndTime:    nowVirtualTime.Unix(),
-		ConditionLevel:   "warning,critical",
-		Limit:            nil,
+		StartTime:      nil,
+		CursorEndTime:  nowVirtualTime.Unix(),
+		ConditionLevel: "warning,critical",
+		Limit:          nil,
 	}
 	// GET condition/{jia_isu_uuid} を取得してバリデーション
 	_, conditions, errs := browserGetIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID,
@@ -304,7 +304,7 @@ func (s *Scenario) requestLastBadConditionScenario(ctx context.Context, step *is
 		case targetIsu.StreamsForScenario.StateChan <- solveCondition:
 		}
 	}
-	
+
 	// LastReadBadConditionTimestamp を更新
 	// condition の順番保障はされてる
 	targetIsu.LastReadBadConditionTimestamp = conditions[0].Timestamp
@@ -364,10 +364,10 @@ func (s *Scenario) getIsuConditionUntilAlreadyRead(
 	// 続きがあり、なおかつ今取得した condition が全て新しい時はスクロールする
 	for {
 		request = service.GetIndividualIsuConditionRequest{
-			StartTime:        request.StartTime,
-			CursorEndTime:    conditions[len(conditions)-1].Timestamp,
-			ConditionLevel:   request.ConditionLevel,
-			Limit:            request.Limit,
+			StartTime:      request.StartTime,
+			CursorEndTime:  conditions[len(conditions)-1].Timestamp,
+			ConditionLevel: request.ConditionLevel,
+			Limit:          request.Limit,
 		}
 		tmpConditions, _, err := getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, request)
 		// poster で送ったものの同期
@@ -428,8 +428,8 @@ func (s *Scenario) requestGraphScenario(ctx context.Context, step *isucandar.Ben
 
 	// scoreの計算
 	for behindDay, gr := range graphResponses {
-		minTimestampCount := int(^uint(0) >> 1) 
-		for _, _ = range gr {			
+		minTimestampCount := int(^uint(0) >> 1)
+		for _, _ = range *gr {
 			// TODO: backend側が graph のレスポンスに根拠timestampを追加したらここで以下のコードを実行する
 			// if len(g.timestamps) < minTimestampCount {
 			// 	minTimestampCount = len(g.timestamps)
@@ -439,7 +439,7 @@ func (s *Scenario) requestGraphScenario(ctx context.Context, step *isucandar.Ben
 
 		}
 		// 「今日のグラフじゃない」&「完成しているグラフ」なら加点
-		if behindDay != 0 && targetIsu.LastCompletedGraphTime <= virtualToday - (int64(behindDay) * OneDay) {
+		if behindDay != 0 && targetIsu.LastCompletedGraphTime <= virtualToday-(int64(behindDay)*OneDay) {
 			// AddScoreはconditionのGETまで待つためここでタグを入れておく
 			scoreTags = append(scoreTags, getGraphScoreTag(minTimestampCount))
 		}
@@ -448,15 +448,15 @@ func (s *Scenario) requestGraphScenario(ctx context.Context, step *isucandar.Ben
 	// データが入ってるレスポンスから、ランダムで見る condition を選ぶ
 	if len(graphResponses) != 0 {
 		// ユーザーが今見ているグラフ
-		nowViewingGraph := graphResponses[len(graphResponses) - 1]
+		nowViewingGraph := graphResponses[len(graphResponses)-1]
 		// チェックする時間
-		checkHour := getCheckHour(nowViewingGraph, randEngine)
+		checkHour := getCheckHour(*nowViewingGraph, randEngine)
 		request := service.GetIndividualIsuConditionRequest{
-			StartTime: &nowViewingGraph[checkHour].StartAt,
-			CursorEndTime: nowViewingGraph[checkHour].EndAt,
+			StartTime:      &(*nowViewingGraph)[checkHour].StartAt,
+			CursorEndTime:  (*nowViewingGraph)[checkHour].EndAt,
 			ConditionLevel: "info,warning,critical",
 		}
-		_, _ , err := getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, request)
+		_, _, err := getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, request)
 		// TODO: retry
 		if err != nil {
 			addErrorWithContext(ctx, step, err)
@@ -477,10 +477,10 @@ func trancateTimestampToDate(timestamp int64) int64 {
 }
 
 // 新しい LastCompletedGraphTime を得る。
-func getNewLastCompletedGraphTime(graphResponses [][]*service.GraphResponse, virtualToday int64) int64 {
+func getNewLastCompletedGraphTime(graphResponses []*service.GraphResponse, virtualToday int64) int64 {
 	var lastCompletedGraphTime int64 = 0
 	for behindDay, gr := range graphResponses {
-		for hour, g := range gr {
+		for hour, g := range *gr {
 			// 12時以降のデータがあるならその前日のグラフは完成している
 			if hour >= 12 && g.Data != nil {
 				completedDay := virtualToday - (OneDay * int64(behindDay))
@@ -494,7 +494,7 @@ func getNewLastCompletedGraphTime(graphResponses [][]*service.GraphResponse, vir
 }
 
 // データが入ってる graph のレスポンスから、ランダムでユーザがチェックする condition を選ぶ
-func getCheckHour(nowViewingGraph []*service.GraphResponse, randEngine *rand.Rand) int {
+func getCheckHour(nowViewingGraph service.GraphResponse, randEngine *rand.Rand) int {
 	dataExistIndexes := []int{}
 	for i, g := range nowViewingGraph {
 		if g.Data != nil {
@@ -529,12 +529,12 @@ func getIsuGraphUntilLastViewed(
 	user *model.User,
 	targetIsu *model.Isu,
 	virtualDay int64,
-) ([][]*service.GraphResponse, []error) {
-	graph := [][]*service.GraphResponse{}
+) ([]*service.GraphResponse, []error) {
+	graph := []*service.GraphResponse{}
 
 	// これで今日のグラフを取る
 	_, todayGraph, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, virtualDay,
-		func(res *http.Response, graph []*service.GraphResponse) []error {
+		func(res *http.Response, graph service.GraphResponse) []error {
 			// TODO: validationは下でやるべき
 			//検証前にデータ取得
 			user.GetConditionFromChan(ctx)
@@ -546,7 +546,7 @@ func getIsuGraphUntilLastViewed(
 		return nil, errs
 	}
 
-	graph = append(graph, todayGraph)
+	graph = append(graph, &todayGraph)
 
 	// 見たグラフまでを見に行く
 	for {
@@ -557,7 +557,7 @@ func getIsuGraphUntilLastViewed(
 			return graph, nil
 		}
 
-		tmpGraph, _, err := getIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, virtualDay)
+		tmpGraph, _, err := getIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, service.GetGraphRequest{Date: virtualDay})
 		if err != nil {
 			return nil, []error{err}
 		}
@@ -565,7 +565,7 @@ func getIsuGraphUntilLastViewed(
 
 		// TODO: validation
 
-		graph = append(graph, tmpGraph)
+		graph = append(graph, &tmpGraph)
 
 		// 作成した時間まで戻ったら終わる
 		if targetIsu.PostTime.Unix() > virtualDay {
@@ -610,10 +610,10 @@ func (s *Scenario) getIsuConditionWithScroll(
 	for i := 0; i < scrollCount && len(conditions) == limit*(i+1); i++ {
 		var conditionsTmp []*service.GetIsuConditionResponse
 		request = service.GetIndividualIsuConditionRequest{
-			StartTime:        request.StartTime,
-			CursorEndTime:    conditions[len(conditions)-1].Timestamp,
-			ConditionLevel:   request.ConditionLevel,
-			Limit:            request.Limit,
+			StartTime:      request.StartTime,
+			CursorEndTime:  conditions[len(conditions)-1].Timestamp,
+			ConditionLevel: request.ConditionLevel,
+			Limit:          request.Limit,
 		}
 		conditionsTmp, res, err := getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, request)
 		if err != nil {
@@ -640,13 +640,18 @@ func getIsuGraphWithPaging(
 	targetIsu *model.Isu,
 	virtualDay int64,
 	scrollCount int,
-) []*service.GraphResponse {
+) service.GraphResponse {
 	//graphを見に行くシナリオ
 	_, graph, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, virtualDay,
-		func(res *http.Response, graph []*service.GraphResponse) []error {
+		func(res *http.Response, graph service.GraphResponse) []error {
 			//検証前にデータ取得
 			user.GetConditionFromChan(ctx)
-			return []error{} //TODO: 検証
+			// graph の検証
+			err := verifyGraph(res, user, targetIsu.JIAIsuUUID, &service.GetGraphRequest{Date: virtualDay}, graph)
+			if err != nil {
+				return []error{err}
+			}
+			return []error{}
 		},
 	)
 	for _, err := range errs {
@@ -660,8 +665,13 @@ func getIsuGraphWithPaging(
 	for i := 0; i < scrollCount; i++ {
 		virtualDay -= 24 * 60 * 60
 		_, graphTmp, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, virtualDay,
-			func(res *http.Response, graph []*service.GraphResponse) []error {
-				return []error{} //TODO: 検証
+			func(res *http.Response, graph service.GraphResponse) []error {
+				// graph の検証
+				err := verifyGraph(res, user, targetIsu.JIAIsuUUID, &service.GetGraphRequest{Date: virtualDay}, graph)
+				if err != nil {
+					return []error{err}
+				}
+				return []error{}
 			},
 		)
 		for _, err := range errs {
@@ -849,10 +859,10 @@ func (s *Scenario) checkCompanyConditionScenario(ctx context.Context, step *isuc
 	scenarioSuccess := true
 	dataExistTimestamp := GetConditionDataExistTimestamp(s, user)
 	request := service.GetIsuConditionRequest{
-		StartTime:        nil,
-		CursorEndTime:    dataExistTimestamp,
-		ConditionLevel:   "warning,critical",
-		Limit:            nil,
+		StartTime:      nil,
+		CursorEndTime:  dataExistTimestamp,
+		ConditionLevel: "warning,critical",
+		Limit:          nil,
 	}
 	conditions, errs := browserGetConditionsAction(ctx, user.Agent,
 		request,
@@ -909,8 +919,13 @@ func (s *Scenario) checkCompanyConditionScenario(ctx context.Context, step *isuc
 			//graphを見る
 			virtualDay := (timestamp / (24 * 60 * 60)) * (24 * 60 * 60)
 			_, _, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsuID, virtualDay,
-				func(res *http.Response, graph []*service.GraphResponse) []error {
-					return []error{} //TODO: 検証
+				func(res *http.Response, getGraphResp service.GraphResponse) []error {
+					// graph の検証
+					err := verifyGraph(res, user, targetIsuID, &service.GetGraphRequest{Date: virtualDay}, getGraphResp)
+					if err != nil {
+						return []error{err}
+					}
+					return []error{}
 				},
 			)
 			for _, err := range errs {
diff --git a/bench/scenario/verify.go b/bench/scenario/verify.go
index b26846ee7..4a75ee036 100644
--- a/bench/scenario/verify.go
+++ b/bench/scenario/verify.go
@@ -364,3 +364,84 @@ func errorChecksum(base string, resource *agent.Resource, name string) error {
 	}
 	return nil
 }
+
+func verifyGraph(
+	res *http.Response, targetUser *model.User, targetIsuUUID string,
+	getGraphReq *service.GetGraphRequest,
+	getGraphResp service.GraphResponse) error {
+
+	// graphResp の配列は必ず 24 つ (24時間分) である
+	if len(getGraphResp) != 24 {
+		return errorInvalid(res, "要素数が正しくありません")
+	}
+
+	var lastStartAt int64
+	// getGraphResp を逆順 (timestamp が新しい順) にloop
+	for idxGraphResp := len(getGraphResp) - 1; idxGraphResp >= 0; idxGraphResp-- {
+		graphOne := getGraphResp[idxGraphResp]
+
+		// getGraphResp の要素が古い順に連続して並んでいることの検証
+		if idxGraphResp != len(getGraphResp)-1 && !(graphOne.EndAt == lastStartAt) {
+			return errorInvalid(res, "整列順が正しくありません")
+		}
+		lastStartAt = graphOne.StartAt
+
+		// 特定の ISU における expected な conditions を新しい順に取得するイテレータを生成
+		targetIsu := targetUser.IsuListByID[targetIsuUUID]
+		filter := model.ConditionLevelInfo | model.ConditionLevelWarning | model.ConditionLevelCritical
+		baseIter := targetIsu.Conditions.LowerBound(filter, graphOne.EndAt, targetIsu.JIAIsuUUID)
+
+		var conditionsBaseOfScore []*model.IsuCondition
+		var lastSort model.IsuConditionCursor
+		// graphOne.ConditionTimestamps を逆順 (timestamp が新しい順) に loop
+		for idxTimestamps := len(graphOne.ConditionTimestamps) - 1; idxTimestamps >= 0; idxTimestamps-- {
+			timestamp := graphOne.ConditionTimestamps[idxTimestamps]
+
+			// graphOne.start_at <= graphOne.condition_timestamps < graphOne.end_at であることの検証
+			if !(graphOne.StartAt <= timestamp && timestamp < graphOne.EndAt) {
+				return errorInvalid(res, "condition_timestampsがstart_atからend_atの中に収まっていません")
+			}
+
+			// graphOne.ConditionTimestamps の要素が古い順に並んでいることの検証
+			nowSort := model.IsuConditionCursor{TimestampUnix: timestamp}
+			if idxTimestamps != len(graphOne.ConditionTimestamps)-1 && !nowSort.Less(&lastSort) {
+				return errorInvalid(res, "整列順が正しくありません")
+			}
+			lastSort = nowSort
+
+			// graphOne.ConditionTimestamps[*] が expected に存在することの検証
+			var expected *model.IsuCondition
+			for {
+				expected = baseIter.Prev()
+				// 降順イテレータから得た expected が timestamp を追い抜いた ⇒ actual が expected に無いデータを返している
+				if expected == nil || expected.TimestampUnix < timestamp {
+					return errorMissmatch(res, "POSTに成功していない時刻のデータが返されました")
+				}
+				if expected.TimestampUnix == timestamp {
+					// graphOne.ConditionTimestamps[n] から condition を取得
+					conditionsBaseOfScore = append(conditionsBaseOfScore, expected)
+					break //ok
+				}
+			}
+		}
+
+		// actual の data が空の場合 verify skip
+		if graphOne.Data == nil {
+			continue
+		}
+
+		// conditionsBaseOfScore から組み立てた data が actual と等値であることの検証
+		expectedGraph := model.NewGraph(conditionsBaseOfScore)
+
+		if graphOne.Data.Score != expectedGraph.Score() ||
+			graphOne.Data.Sitting != expectedGraph.Sitting() ||
+			graphOne.Data.Detail["is_broken"] != expectedGraph.IsBroken() ||
+			graphOne.Data.Detail["is_dirty"] != expectedGraph.IsDirty() ||
+			graphOne.Data.Detail["is_overweight"] != expectedGraph.IsOverweight() ||
+			graphOne.Data.Detail["missing_data"] != expectedGraph.MissingData() {
+			return errorMissmatch(res, "graphのデータが正しくありません")
+		}
+	}
+
+	return nil
+}
diff --git a/bench/service/request.go b/bench/service/request.go
index 03dca6f11..faf306ff7 100644
--- a/bench/service/request.go
+++ b/bench/service/request.go
@@ -53,8 +53,12 @@ type GetIsuConditionRequest struct {
 }
 
 type GetIndividualIsuConditionRequest struct {
-	StartTime        *int64
-	CursorEndTime    int64
-	ConditionLevel   string
-	Limit            *int
+	StartTime      *int64
+	CursorEndTime  int64
+	ConditionLevel string
+	Limit          *int
+}
+
+type GetGraphRequest struct {
+	Date int64 // unixtime
 }
diff --git a/bench/service/response.go b/bench/service/response.go
index c62d8eb53..78fa826ac 100644
--- a/bench/service/response.go
+++ b/bench/service/response.go
@@ -42,10 +42,13 @@ type GetIsuConditionResponse struct {
 	Message        string `json:"message"`
 }
 
-type GraphResponse struct {
-	StartAt int64      `json:"start_at"`
-	EndAt   int64      `json:"end_at"`
-	Data    *GraphData `json:"data"`
+type GraphResponse []*GraphResponseOne
+
+type GraphResponseOne struct {
+	StartAt             int64      `json:"start_at"`
+	EndAt               int64      `json:"end_at"`
+	Data                *GraphData `json:"data"`
+	ConditionTimestamps []int64    `json:"condition_timestamps"`
 }
 
 type GraphData struct {


Test patch: