From cd3d6643235ecaaa98ddff79bbc55f06524f6541 Mon Sep 17 00:00:00 2001 From: jan-molak Date: Mon, 9 Feb 2015 23:11:00 +0000 Subject: [PATCH] (feature) Instead of the average build time, time elapsed since last build is displayed. The average build time is now only displayed when the build is in progress. Closes #63 and closes #120. --- .../buildmonitor/viewmodel/BuildView.java | 14 ++++- .../viewmodel/BuildViewModel.java | 2 + .../buildmonitor/viewmodel/JobView.java | 6 ++ .../buildmonitor/viewmodel/NullBuildView.java | 6 ++ .../viewmodel/duration/Duration.java | 20 +++++++ .../duration/DurationInMilliseconds.java | 13 +++++ .../HumanReadableDuration.java} | 30 ++++------ .../buildmonitor/BuildMonitorView/index.jelly | 1 + .../BuildMonitorView/main-jobViews.jelly | 8 ++- src/main/webapp/scripts/app.js | 7 ++- src/main/webapp/scripts/filters.js | 42 ++++++++++++++ .../buildmonitor/viewmodel/JobViewTest.java | 18 +++++- .../filters/estimatedTimeElapsedSinceSpec.js | 57 +++++++++++++++++++ 13 files changed, 195 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/Duration.java create mode 100644 src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/DurationInMilliseconds.java rename src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/{Duration.java => duration/HumanReadableDuration.java} (61%) create mode 100644 src/main/webapp/scripts/filters.js create mode 100644 src/test/javascript/unit/filters/estimatedTimeElapsedSinceSpec.js diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildView.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildView.java index a4d850679..39ce26fff 100644 --- a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildView.java +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildView.java @@ -1,6 +1,9 @@ package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.facade.RelativeLocation; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.Duration; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.HumanReadableDuration; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.DurationInMilliseconds; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.plugins.BuildAugmentor; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.plugins.bfa.Analysis; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.plugins.claim.Claim; @@ -60,17 +63,22 @@ private boolean isRunning(Run build) { @Override public Duration elapsedTime() { - return new Duration(now() - whenTheBuildStarted()); + return new HumanReadableDuration(now() - whenTheBuildStarted()); + } + + @Override + public Duration timeElapsedSince() { + return new DurationInMilliseconds(now() - (whenTheBuildStarted() + build.getDuration())); } @Override public Duration duration() { - return new Duration(build.getDuration()); + return new HumanReadableDuration(build.getDuration()); } @Override public Duration estimatedDuration() { - return new Duration(build.getEstimatedDuration()); + return new HumanReadableDuration(build.getEstimatedDuration()); } @Override diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildViewModel.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildViewModel.java index 577e4b982..5a0f4f5fd 100644 --- a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildViewModel.java +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/BuildViewModel.java @@ -1,5 +1,6 @@ package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.Duration; import hudson.model.Result; import java.util.List; @@ -12,6 +13,7 @@ public interface BuildViewModel { public boolean isRunning(); public Duration elapsedTime(); + public Duration timeElapsedSince(); public Duration duration(); public Duration estimatedDuration(); public int progress(); diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobView.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobView.java index 52fef0222..309123fa3 100644 --- a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobView.java +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobView.java @@ -1,6 +1,7 @@ package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.facade.RelativeLocation; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.Duration; import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.plugins.BuildAugmentor; import hudson.model.Job; import hudson.model.Result; @@ -96,6 +97,11 @@ public String estimatedDuration() { return formatted(lastBuild().estimatedDuration()); } + @JsonProperty + public String timeElapsedSinceLastBuild() { + return formatted(lastCompletedBuild().timeElapsedSince()); + } + private String formatted(Duration duration) { return null != duration ? duration.toString() diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/NullBuildView.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/NullBuildView.java index 6484e2e8d..3a1aa15cb 100644 --- a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/NullBuildView.java +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/NullBuildView.java @@ -1,5 +1,6 @@ package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel; +import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.Duration; import hudson.model.Result; import java.util.HashSet; @@ -33,6 +34,11 @@ public Duration elapsedTime() { return null; } + @Override + public Duration timeElapsedSince() { + return null; + } + @Override public Duration duration() { return null; diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/Duration.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/Duration.java new file mode 100644 index 000000000..0e17f114b --- /dev/null +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/Duration.java @@ -0,0 +1,20 @@ +package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration; + +abstract public class Duration { + + protected final long duration; + + public Duration(long milliseconds) { + this.duration = milliseconds; + } + + abstract public String toString(); + + public boolean greaterThan(Duration otherDuration) { + return duration > otherDuration.toLong(); + } + + private long toLong() { + return duration; + } +} \ No newline at end of file diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/DurationInMilliseconds.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/DurationInMilliseconds.java new file mode 100644 index 000000000..bef5b9442 --- /dev/null +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/DurationInMilliseconds.java @@ -0,0 +1,13 @@ +package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration; + +public class DurationInMilliseconds extends Duration { + + public DurationInMilliseconds(long milliseconds) { + super(milliseconds); + } + + @Override + public String toString() { + return String.valueOf(duration); + } +} diff --git a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/Duration.java b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/HumanReadableDuration.java similarity index 61% rename from src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/Duration.java rename to src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/HumanReadableDuration.java index 22d1d4039..8e7d8d550 100644 --- a/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/Duration.java +++ b/src/main/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/duration/HumanReadableDuration.java @@ -1,34 +1,29 @@ -package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel; - -class Duration {//todo: extract the Duration class, or move it to a BuildView class - - private final long duration; +package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration; +// todo: is this still needed? backend could pass the duration in milliseconds to the frontend, which in turn could make it human-readable +public class HumanReadableDuration extends Duration { private final static long MILLISECOND = 1; private final static long SECONDS = 1000 * MILLISECOND; private final static long MINUTES = 60 * SECONDS; private final static long HOURS = 60 * MINUTES; - public Duration(long milliseconds) { - this.duration = milliseconds; + public HumanReadableDuration(long milliseconds) { + super(milliseconds); } - public long hours() { + private long hours() { return duration / HOURS; } - public long minutes() { + private long minutes() { return (duration - (hours() * HOURS)) / MINUTES; } - public long seconds() { + private long seconds() { return (duration - (hours() * HOURS) - (minutes() * MINUTES)) / SECONDS; } - public boolean greaterThan(Duration otherDuration) { - return duration > otherDuration.toLong(); - } - + @Override public String toString() { String formatted; @@ -38,9 +33,4 @@ public String toString() { return formatted; } - - - private long toLong() { - return duration; - } -} \ No newline at end of file +} diff --git a/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/index.jelly b/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/index.jelly index 130632c8e..0b2fe5a53 100644 --- a/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/index.jelly +++ b/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/index.jelly @@ -85,6 +85,7 @@ + diff --git a/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/main-jobViews.jelly b/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/main-jobViews.jelly index 571425011..849201937 100644 --- a/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/main-jobViews.jelly +++ b/src/main/resources/com/smartcodeltd/jenkinsci/plugins/buildmonitor/BuildMonitorView/main-jobViews.jelly @@ -29,10 +29,14 @@ title="Details of {{job.name}}, build {{job.lastBuildName}}" href="{{job.lastBuildUrl}}">{{job.lastBuildName}} -
- {{job.lastBuildDuration}} +
+ {{job.lastBuildDuration}} {{job.estimatedDuration}}
+ +
+ {{job.timeElapsedSinceLastBuild | estimatedTimeElapsedSince}} +
\ No newline at end of file diff --git a/src/main/webapp/scripts/app.js b/src/main/webapp/scripts/app.js index dcea060f1..f37545660 100644 --- a/src/main/webapp/scripts/app.js +++ b/src/main/webapp/scripts/app.js @@ -1,7 +1,12 @@ 'use strict'; angular. - module('buildMonitor', [ 'buildMonitor.controllers', 'buildMonitor.settings', 'slugifier' ]). + module('buildMonitor', [ + 'buildMonitor.controllers', + 'buildMonitor.filters', + 'buildMonitor.settings', + 'slugifier' + ]). run(['$rootScope', 'notifyUser', function($rootScope, notifyUser) { $rootScope.settings = { }; diff --git a/src/main/webapp/scripts/filters.js b/src/main/webapp/scripts/filters.js new file mode 100644 index 000000000..4147af39c --- /dev/null +++ b/src/main/webapp/scripts/filters.js @@ -0,0 +1,42 @@ +'use strict'; + +angular. + module('buildMonitor.filters', []). + + filter('estimatedTimeElapsedSince', function() { + var seconds = 1000, + minutes = 60 * seconds, + hours = 60 * minutes, + days = 24 * hours, + months = 30 * days, + years = 12 * months, + + unitsOfTime = [ + { divisor: seconds, singular: "%d second ago", plural: "%d seconds ago" }, + { divisor: minutes / seconds, singular: "%d minute ago", plural: "%d minutes ago" }, + { divisor: hours / minutes, singular: "%d hour ago", plural: "%d hours ago" }, + { divisor: days / hours, singular: "%d day ago", plural: "%d days ago" }, + { divisor: months / days, singular: "%d month ago", plural: "%d months ago" }, + { divisor: years / months, singular: "over a year ago", plural: "hasn't run in ages!" } + ]; + + function humanFriendly(remainder, unitOfTime) { + var rounded = Math.round(remainder); + + return (rounded === 1 ? unitOfTime.singular : unitOfTime.plural).replace("%d", rounded); + } + + function approximate(remainder, unitOfTime, tail) { + return (tail.length === 0 || remainder < tail[0].divisor) ? + humanFriendly(remainder, unitOfTime) : + approximate(remainder / tail[0].divisor, tail[0], tail.slice(1)); + } + + return function(ago) { + switch(true) { + case (ago <= 30 * seconds): return "just now :-)"; + case (ago <= 5 * minutes): return "a moment ago"; + default: return approximate(ago / unitsOfTime[0].divisor, unitsOfTime[0], unitsOfTime.slice(1)); + } + } + }); \ No newline at end of file diff --git a/src/test/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobViewTest.java b/src/test/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobViewTest.java index 406fb1196..d84d0d9d6 100644 --- a/src/test/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobViewTest.java +++ b/src/test/java/com/smartcodeltd/jenkinsci/plugins/buildmonitor/viewmodel/JobViewTest.java @@ -9,7 +9,6 @@ import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.syntacticsugar.JobStateRecipe; import hudson.model.Job; import hudson.model.Result; -import org.junit.Ignore; import org.junit.Test; import java.text.ParseException; @@ -40,7 +39,6 @@ public class JobViewTest { private RelativeLocation relative = mock(RelativeLocation.class); private JobView view; - /* * By the way, if you were not aware of this: the configuration page of each job has an "Advanced Project Options" * section, where you can set a user-friendly "Display Name" @@ -194,7 +192,21 @@ public void should_not_say_anything_if_it_doesnt_know_how_long_the_next_build_is assertThat(view.estimatedDuration(), is("")); } - + + /* + * Last build, last success and last failure (ISO 8601) + */ + @Test + public void should_know_how_long_since_the_last_build_happened() throws Exception { + String tenMinutesInMilliseconds = String.format("%d", 10 * 60 * 1000); + + view = JobView.of(a(job().whereTheLast(build().startedAt("18:05:00").andTook(5))), + assumingThatCurrentTimeIs("18:20:00") + ); + + assertThat(view.timeElapsedSinceLastBuild(), is(tenMinutesInMilliseconds)); + } + /* * Should produce a meaningful status description that can be used in the CSS */ diff --git a/src/test/javascript/unit/filters/estimatedTimeElapsedSinceSpec.js b/src/test/javascript/unit/filters/estimatedTimeElapsedSinceSpec.js new file mode 100644 index 000000000..79b21da8f --- /dev/null +++ b/src/test/javascript/unit/filters/estimatedTimeElapsedSinceSpec.js @@ -0,0 +1,57 @@ +'use strict'; + +describe('buildMonitor', function () { + describe('buildMonitor.filters', function () { + describe('estimatedTimeElapsedSince', function () { + var milliseconds = 1, + seconds = 1000 * milliseconds, + minutes = 60 * seconds, + hours = 60 * minutes, + days = 24 * hours, + months = 30 * days, + years = 12 * months; + + beforeEach(module('buildMonitor.filters')); + + it('converts time in milliseconds to something more user-friendly (under 5 minutes)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter(0 * minutes)).toEqual("just now :-)"); + expect(estimatedTimeElapsedSinceFilter(0.5 * minutes)).toEqual("just now :-)"); + expect(estimatedTimeElapsedSinceFilter(3 * minutes)).toEqual("a moment ago"); + expect(estimatedTimeElapsedSinceFilter(4.5 * minutes)).toEqual("a moment ago"); + })); + + it('approximates to the nearest minute (under 1 hour)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter( 5.5 * minutes)).toEqual("6 minutes ago"); + expect(estimatedTimeElapsedSinceFilter(10.75 * minutes)).toEqual("11 minutes ago"); + expect(estimatedTimeElapsedSinceFilter(30 * minutes)).toEqual("30 minutes ago"); + expect(estimatedTimeElapsedSinceFilter(59 * minutes)).toEqual("59 minutes ago"); + })); + + it('approximates to the nearest hour (under 1 day)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter(61 * minutes)).toEqual("1 hour ago"); + expect(estimatedTimeElapsedSinceFilter(1.3 * hours )).toEqual("1 hour ago"); + expect(estimatedTimeElapsedSinceFilter(1.5 * hours )).toEqual("2 hours ago"); + expect(estimatedTimeElapsedSinceFilter( 2 * hours )).toEqual("2 hours ago"); + expect(estimatedTimeElapsedSinceFilter(17.61 * hours )).toEqual("18 hours ago"); + })); + + it('approximates to the nearest day (under 1 month)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter( 1.25 * days)).toEqual("1 day ago"); + expect(estimatedTimeElapsedSinceFilter( 2 * days)).toEqual("2 days ago"); + expect(estimatedTimeElapsedSinceFilter( 7 * days)).toEqual("7 days ago"); + })); + + it('approximates to the nearest month (under 1 year)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter(31 * days )).toEqual("1 month ago"); + expect(estimatedTimeElapsedSinceFilter( 2.3 * months)).toEqual("2 months ago"); + expect(estimatedTimeElapsedSinceFilter(11.7 * months)).toEqual("12 months ago"); + })); + + it('approximates to the nearest year (boy, I really hope no one needs this range!)', inject(function (estimatedTimeElapsedSinceFilter) { + expect(estimatedTimeElapsedSinceFilter(13 * months)).toEqual("over a year ago"); + expect(estimatedTimeElapsedSinceFilter(25 * months)).toEqual("hasn't run in ages!"); + expect(estimatedTimeElapsedSinceFilter(5 * years)).toEqual("hasn't run in ages!"); + })); + }); + }); +}); \ No newline at end of file