From 6c80eb1a7539419aaf1fa8afd14e35648f7a4257 Mon Sep 17 00:00:00 2001 From: Sebastian Wilzbach Date: Sun, 16 Jul 2017 21:16:58 +0200 Subject: [PATCH 1/2] Rename GHCiStatus -> GHStatuses --- source/dlangbot/github.d | 4 ++-- source/dlangbot/github_api.d | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/source/dlangbot/github.d b/source/dlangbot/github.d index 9f25bf6..9638a8b 100644 --- a/source/dlangbot/github.d +++ b/source/dlangbot/github.d @@ -360,8 +360,8 @@ void searchForInactivePrs(string repoSlug, Duration dur) // label PR with persistent CI failures auto status = pr.status; auto failCount = status.filter!((e){ - if (e.state == GHCiStatus.State.failure || - e.state == GHCiStatus.State.error) + if (e.state == GHStatuses.State.failure || + e.state == GHStatuses.State.error) switch (e.context) { case "auto-tester": case "CyberShadow/DAutoTest": diff --git a/source/dlangbot/github_api.d b/source/dlangbot/github_api.d index 82ab94e..3593490 100644 --- a/source/dlangbot/github_api.d +++ b/source/dlangbot/github_api.d @@ -200,10 +200,10 @@ struct PullRequest .readJson .deserializeJson!(GHReview[]); } - GHCiStatus[] status() const { + GHStatuses[] status() const { return ghGetRequest(statusURL) .readJson["statuses"] - .deserializeJson!(GHCiStatus[]); + .deserializeJson!(GHStatuses[]); } GHLabel[] labels() const { @@ -288,12 +288,13 @@ struct GHCommit GHUser committer; } -struct GHCiStatus +// https://developer.github.com/v3/repos/statuses/ +struct GHStatuses { enum State { success, error, failure, pending } @byName State state; string description; - @name("target_url") string targetUrl; + @name("target_url") string targetURL; string context; // "CyberShadow/DAutoTest", "Project Tester", // "ci/circleci", "auto-tester", "codecov/project", // "codecov/patch", "continuous-integration/travis-ci/pr" From 154617c7c252ee6e778bb9811a34309b75923f70 Mon Sep 17 00:00:00 2001 From: Sebastian Wilzbach Date: Sun, 16 Jul 2017 21:17:14 +0200 Subject: [PATCH 2/2] Add first CodeCov test --- data/hooks/codecov/dlang_druntime_1877.json | 273 ++++++++++++++++++++ source/dlangbot/app.d | 4 +- source/dlangbot/codecov.d | 125 +++++++++ test/codecov.d | 15 ++ test/utils.d | 34 +++ 5 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 data/hooks/codecov/dlang_druntime_1877.json create mode 100644 source/dlangbot/codecov.d create mode 100644 test/codecov.d diff --git a/data/hooks/codecov/dlang_druntime_1877.json b/data/hooks/codecov/dlang_druntime_1877.json new file mode 100644 index 0000000..fde358f --- /dev/null +++ b/data/hooks/codecov/dlang_druntime_1877.json @@ -0,0 +1,273 @@ +{ + "compare": { + "message": "increased", + "url": "https://codecov.io/gh/dlang/druntime/compare/a3627aa018874ff207392a8fce9eaf174dc2202b...d4015f4d403bd22ecee8b81bb61378d33e7df7df", + "notation": "+", + "coverage": "0.08" + }, + "owner": { + "service_id": "565913", + "username": "dlang", + "service": "github" + }, + "pull": { + "title": "Merge remote-tracking branch 'upstream/mangle' into merge_mangle", + "id": "1877", + "state": "open", + "number": "1877", + "head": { + "commitid": "d4015f4d403bd22ecee8b81bb61378d33e7df7df", + "branch": "merge_mangle" + }, + "base": { + "commitid": "a3627aa018874ff207392a8fce9eaf174dc2202b", + "branch": "master" + } + }, + "head": { + "message": "Merge remote-tracking branch 'upstream/mangle' into merge_mangle", + "version": 3, + "timestamp": "2017-07-16 13:43:16", + "branch": "merge_mangle", + "ci_passed": true, + "pullid": "1877", + "service_url": "https://github.com/dlang/druntime/commit/d4015f4d403bd22ecee8b81bb61378d33e7df7df", + "changes": [ + [ + "src/gc/impl/conservative/gc.d", + null, + null, + false, + null, + [ + 0, + 0, + -2, + 2, + 0, + -0.17841000000001372, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + [ + "src/core/thread.d", + null, + null, + false, + null, + [ + 0, + 0, + -3, + 3, + 0, + -0.3278700000000043, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + [ + "src/core/sync/semaphore.d", + null, + null, + false, + null, + [ + 0, + 0, + 1, + -1, + 0, + 1.2195099999999996, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + [ + "src/core/demangle.d", + null, + null, + true, + null, + [ + 0, + 0, + 26, + -26, + 0, + 3.3333300000000037, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ] + ], + "logs": [ + { + "event": "upload", + "build": "943", + "ci": "circleci", + "time": "2017-07-16 13:46:47.433296" + } + ], + "archived": true, + "totals": { + "M": 0, + "s": 1, + "p": 0, + "N": 0, + "C": 0, + "h": 13367, + "n": 17647, + "m": 4280, + "c": "75.74659", + "b": 0, + "f": 137, + "d": 0 + }, + "notified": null, + "parent_totals": { + "m": 4229, + "diff": [ + 1, + 29, + 29, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "s": 1, + "p": 0, + "N": 0, + "C": 0, + "h": 13148, + "n": 17377, + "M": 0, + "c": "75.66323", + "b": 0, + "f": 137, + "d": 0 + }, + "merged": false, + "author": { + "name": "Martin Nowak", + "service_id": "288976", + "service": "github", + "username": "MartinNowak", + "email": "removed@removed.com" + }, + "commitid": "d4015f4d403bd22ecee8b81bb61378d33e7df7df", + "deleted": null, + "parent": "a3627aa018874ff207392a8fce9eaf174dc2202b", + "state": "complete", + "updatestamp": "2017-07-16 13:53:57.426479", + "url": "https://codecov.io/gh/dlang/druntime/commit/d4015f4d403bd22ecee8b81bb61378d33e7df7df" + }, + "base": { + "parent_totals": { + "m": 4231, + "diff": [ + 1, + 29, + 29, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "s": 1, + "p": 0, + "n": 17377, + "C": 0, + "h": 13146, + "N": 0, + "M": 0, + "c": "75.65172", + "b": 0, + "f": 137, + "d": 0 + }, + "notified": true, + "message": "Merge pull request #1875 from rainers/gc_coverage\n\nGC: add tests to make coverage a bit more deterministic", + "version": 3, + "timestamp": "2017-07-16 13:01:39", + "archived": true, + "totals": { + "M": 0, + "s": 1, + "p": 0, + "N": 0, + "C": 0, + "h": 13148, + "n": 17377, + "m": 4229, + "c": "75.66323", + "b": 0, + "f": 137, + "d": 0 + }, + "ci_passed": true, + "state": "complete", + "merged": false, + "author": { + "name": "Andrei Alexandrescu", + "service_id": "566679", + "service": "github", + "username": "andralex", + "email": "removed@removed.com" + }, + "commitid": "a3627aa018874ff207392a8fce9eaf174dc2202b", + "deleted": null, + "parent": "692bcade6508c1da10a41058f668048a36b606c5", + "pullid": null, + "logs": [ + { + "event": "upload", + "build": "941", + "ci": "circleci", + "time": "2017-07-16 13:04:36.007785" + } + ], + "updatestamp": "2017-07-16 13:10:53.611332", + "branch": "master" + }, + "repo": { + "service_id": "1257087", + "private": false, + "url": "https://codecov.io/gh/dlang/druntime", + "name": "druntime" + } +} diff --git a/source/dlangbot/app.d b/source/dlangbot/app.d index 8d03f2c..4a98edd 100644 --- a/source/dlangbot/app.d +++ b/source/dlangbot/app.d @@ -232,7 +232,9 @@ void handlePR(string action, PullRequest* _pr) void codecovHook(HTTPServerRequest req, HTTPServerResponse res) { - logDebug("codecovHook: %s", req.bodyReader.readAllUTF8); + import dlangbot.codecov; + auto payload = req.bodyReader.readAllUTF8.parseJsonString.deserializeJson!CodeCovHook; + runTaskHelper(&handleCodecovPR, &payload); return res.writeBody("OK"); } diff --git a/source/dlangbot/codecov.d b/source/dlangbot/codecov.d new file mode 100644 index 0000000..d6693dc --- /dev/null +++ b/source/dlangbot/codecov.d @@ -0,0 +1,125 @@ +module dlangbot.codecov; + +import dlangbot.github_api; + +import std.datetime : SysTime; +import std.typecons : Nullable; + +import vibe.core.log; +import vibe.data.json; + +// codecov sends strings for numbers +struct CodeCovHook +{ + static struct CodeCovCompare + { + string message, url, notation; + string coverage; // TODO: convert + } + CodeCovCompare compare; + + struct CodeCovRepo + { + @name("commitid") string sha; + string branch; + } + struct CodeCovPull + { + string title; + string id; // TODO: convert + string state; + string number; // TODO: convert + CodeCovRepo head; + CodeCovRepo base; + } + @optional Nullable!CodeCovPull pull; + + struct CodeCovTotals + { + @name("c") string coverage; // TODO: convert + /* + TODO: quite cryptic. + "p":0, + "s":1, + "diff":null, + "m":4254, + "b":0, + "C":0, + "d":0, + "n":17400, + "f":136, + "h":13146, + "c":"75.55172", + "M":0, + "N":0 + */ + } + + struct CodeCovFullRepo + { + string message; + uint version_; + string branch; + @name("parent") string parentSha; + @optional @name("pullid") Nullable!string pullId; // TODO: convert + string state; + + // CodeCov doesn't send ISO strings + //SysTime timestamp; + //@optional Nullable!SysTime updatestamp; + + @name("ci_passed") bool passed; + @optional @name("service_url") Nullable!string serviceURL; + // CodeCov sends booleans as strings + //@optional Nullable!bool notified; + //@optional Nullable!bool archived; + //@optional Nullable!bool deleted; + + CodeCovTotals totals; + @name("parent_totals") CodeCovTotals parentTotals; + + // Not needed: author, logs + } + + CodeCovFullRepo base; + CodeCovFullRepo head; + + string repoSlug() + { + return owner.name ~ "/" ~ repo.name; + } + + static struct CodeCovOwner + { + import vibe.data.json : Name = name; + @Name("username") string name; + } + CodeCovOwner owner; + + static struct CodeCovRepoInfo + { + string name; + } + CodeCovRepoInfo repo; +} + +void handleCodecovPR(CodeCovHook* _hook) +{ + auto hook = *_hook; + import std.stdio; + import std.format : format; + if (hook.pull.isNull) + return; + + logDebug("[codecov/handleStatus](%s): sha=%s", hook.repoSlug, hook.pull.head.sha); + GHStatuses status = { + state: GHStatuses.State.success, + targetURL: hook.compare.url, + description: "Total coverage: %s (%s)".format(hook.compare.message, hook.compare.coverage), + context: "dlangbot/codecov", + }; + logDebug("[codecov/handleStatus](%s): status=%s", status); + ghSendRequest((scope req){ + req.writeJsonBody(status); + }, githubAPIURL ~ "/repos/%s/statuses/%s".format(hook.repoSlug, hook.pull.head.sha)); +} diff --git a/test/codecov.d b/test/codecov.d new file mode 100644 index 0000000..83086d5 --- /dev/null +++ b/test/codecov.d @@ -0,0 +1,15 @@ +import utils; + +unittest +{ + setAPIExpectations( + "/github/repos/dlang/druntime/statuses/d4015f4d403bd22ecee8b81bb61378d33e7df7df", + (scope HTTPServerRequest req, scope HTTPServerResponse res){ + import std.stdio; + assert(req.json["message"].get!string == "Total coverage: increased (+0.08)"); + assert(req.json["context"].get!string == "dlangbot/codecov"); + assert(req.json["state"].get!string == "success"); + } + ); + postCodeCovHook("dlang_druntime_1877.json"); +} diff --git a/test/utils.d b/test/utils.d index b48ceda..5ed9554 100644 --- a/test/utils.d +++ b/test/utils.d @@ -18,6 +18,7 @@ public import std.algorithm; string testServerURL; string ghTestHookURL; string trelloTestHookURL; +string codecovTestHookURL; string payloadDir = "./data/payloads"; string hookDir = "./data/hooks"; @@ -51,6 +52,7 @@ shared static this() ~ settings.port.to!string; ghTestHookURL = testServerURL ~ "/github_hook"; trelloTestHookURL = testServerURL ~ "/trello_hook"; + codecovTestHookURL = testServerURL ~ "/codecov_hook"; import vibe.core.log; setLogLevel(LogLevel.info); @@ -299,6 +301,38 @@ void postTrelloHook(string payload, checkAPIExpectations; } +void postCodeCovHook(string payload, + void delegate(ref Json j, scope HTTPClientRequest req) postprocess = null, + int line = __LINE__, string file = __FILE__) +{ + import std.file : readText; + import std.path : buildPath; + import dlangbot.trello : getSignature; + + payload = hookDir.buildPath("codecov", payload); + + logInfo("Starting test in %s:%d with payload: %s", file, line, payload); + + auto req = requestHTTP(codecovTestHookURL, (scope req) { + req.method = HTTPMethod.POST; + + auto payload = payload.readText.parseJsonString; + + if (postprocess !is null) + postprocess(payload, req); + + auto respStr = payload.toString; + req.writeBody(cast(ubyte[]) respStr); + }); + scope(failure) { + if (req.statusCode != 200) + writeln(req.bodyReader.readAllUTF8); + } + assert(req.statusCode == 200); + assert(req.bodyReader.readAllUTF8 == "OK"); + checkAPIExpectations; +} + void openUrl(string url, string expectedResponse, int line = __LINE__, string file = __FILE__) {