From 4b3e8b8694b8ebd5e3d4701c653e7f358a239014 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 17:38:55 -0500 Subject: [PATCH 1/5] Store version build info uploaded by cargo in the database --- src/bin/migrate.rs | 15 +++++++ src/lib.rs | 1 + src/tests/version.rs | 92 +++++++++++++++++++++++++++++++++++++++++ src/upload.rs | 15 +++++++ src/version.rs | 98 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 218 insertions(+), 3 deletions(-) diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index ce635d3db5a..df529c1e4a4 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -831,6 +831,21 @@ fn migrations() -> Vec { try!(tx.execute("DROP INDEX badges_crate_type", &[])); Ok(()) }), + Migration::add_table(20170127104519, "build_info", " \ + version_id INTEGER NOT NULL, \ + rust_version VARCHAR NOT NULL, \ + target VARCHAR NOT NULL, \ + passed BOOLEAN NOT NULL, \ + created_at TIMESTAMP NOT NULL DEFAULT now(), \ + updated_at TIMESTAMP NOT NULL DEFAULT now()"), + Migration::new(20170127104925, |tx| { + try!(tx.execute("CREATE UNIQUE INDEX build_info_combo \ + ON build_info (version_id, rust_version, target)", &[])); + Ok(()) + }, |tx| { + try!(tx.execute("DROP INDEX build_info_combo", &[])); + Ok(()) + }), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` diff --git a/src/lib.rs b/src/lib.rs index f3efe3fedb2..bdc8df74a98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,6 +101,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners)); api_router.delete("/crates/:crate_id/:version/yank", C(version::yank)); api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank)); + api_router.put("/crates/:crate_id/:version/build_info", C(version::publish_build_info)); api_router.get("/crates/:crate_id/reverse_dependencies", C(krate::reverse_dependencies)); api_router.get("/versions", C(version::index)); api_router.get("/versions/:version_id", C(version::show)); diff --git a/src/tests/version.rs b/src/tests/version.rs index 5e4b13c7de5..ada1378872f 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -70,3 +70,95 @@ fn authors() { let json = json.as_object().unwrap(); assert!(json.contains_key(&"users".to_string())); } + +#[test] +fn publish_build_info() { + #[derive(RustcDecodable)] struct O { ok: bool } + let (_b, app, middle) = ::app(); + + let mut req = ::new_req(app.clone(), "publish-build-info", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + ::mock_crate(&mut req, ::krate("publish-build-info")); + + let body = "{\ + \"name\":\"publish-build-info\",\ + \"vers\":\"1.0.0\",\ + \"rust_version\":\"rustc 1.16.0-nightly (df8debf6d 2017-01-25)\",\ + \"target\":\"x86_64-pc-windows-gnu\",\ + \"passed\":true}"; + + let mut response = ok_resp!(middle.call(req.with_path( + "/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(::json::(&mut response).ok); + + let body = "{\ + \"name\":\"publish-build-info\",\ + \"vers\":\"1.0.0\",\ + \"rust_version\":\"rustc 1.13.0 (df8debf6d 2017-01-25)\",\ + \"target\":\"x86_64-pc-windows-gnu\",\ + \"passed\":true}"; + + let mut response = ok_resp!(middle.call(req.with_path( + "/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(::json::(&mut response).ok); + + let body = "{\ + \"name\":\"publish-build-info\",\ + \"vers\":\"1.0.0\",\ + \"rust_version\":\"rustc 1.15.0-beta (df8debf6d 2017-01-20)\",\ + \"target\":\"x86_64-pc-windows-gnu\",\ + \"passed\":true}"; + + let mut response = ok_resp!(middle.call(req.with_path( + "/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(::json::(&mut response).ok); +} + +#[test] +fn bad_rust_version_publish_build_info() { + let (_b, app, middle) = ::app(); + + let mut req = ::new_req(app.clone(), "bad-rust-vers", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + ::mock_crate(&mut req, ::krate("bad-rust-vers")); + + let body = "{\ + \"name\":\"bad-rust-vers\",\ + \"vers\":\"1.0.0\",\ + \"rust_version\":\"rustc 1.16.0-dev (df8debf6d 2017-01-25)\",\ + \"target\":\"x86_64-pc-windows-gnu\",\ + \"passed\":true}"; + + let response = bad_resp!(middle.call(req.with_path( + "/api/v1/crates/bad-rust-vers/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + assert_eq!( + response.errors[0].detail, + "rust_version `rustc 1.16.0-dev (df8debf6d 2017-01-25)` \ + not recognized as nightly, beta, or stable"); + + let body = "{\ + \"name\":\"bad-rust-vers\",\ + \"vers\":\"1.0.0\",\ + \"rust_version\":\"1.15.0\",\ + \"target\":\"x86_64-pc-windows-gnu\",\ + \"passed\":true}"; + + let response = bad_resp!(middle.call(req.with_path( + "/api/v1/crates/bad-rust-vers/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + assert_eq!( + response.errors[0].detail, + "rust_version `1.15.0` not recognized; \ + expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`"); +} diff --git a/src/upload.rs b/src/upload.rs index 3a775645f6e..c98418cc149 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -4,9 +4,11 @@ use std::ops::Deref; use rustc_serialize::{Decodable, Decoder, Encoder, Encodable}; use semver; use dependency::Kind as DependencyKind; +use util::CargoResult; use keyword::Keyword as CrateKeyword; use krate::Crate; +use version::ChannelVersion; #[derive(RustcDecodable, RustcEncodable)] pub struct NewCrate { @@ -48,6 +50,19 @@ pub struct CrateDependency { pub kind: Option, } +#[derive(RustcDecodable, RustcEncodable)] +pub struct VersionBuildInfo { + pub rust_version: String, + pub target: String, + pub passed: bool, +} + +impl VersionBuildInfo { + pub fn channel_version(&self) -> CargoResult { + self.rust_version.parse() + } +} + impl Decodable for CrateName { fn decode(d: &mut D) -> Result { let s = try!(d.read_str()); diff --git a/src/version.rs b/src/version.rs index 2265d445db8..dbdf357c0e5 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::collections::HashMap; use conduit::{Request, Response}; @@ -6,8 +7,7 @@ use pg::GenericConnection; use pg::rows::Row; use rustc_serialize::json; use semver; -use time::Duration; -use time::Timespec; +use time::{self, Duration, Timespec}; use url; use {Model, Crate, User}; @@ -19,7 +19,7 @@ use git; use upload; use user::RequestUser; use owner::{rights, Rights}; -use util::{RequestUtils, CargoResult, ChainError, internal, human}; +use util::{RequestUtils, CargoResult, ChainError, internal, human, CargoError}; #[derive(Clone)] pub struct Version { @@ -59,6 +59,50 @@ pub struct VersionLinks { pub authors: String, } +pub enum ChannelVersion { + Stable(semver::Version), + Beta(Timespec), + Nightly(Timespec), +} + +impl FromStr for ChannelVersion { + type Err = Box; + + fn from_str(s: &str) -> CargoResult { + // Recognized formats: + // rustc 1.14.0 (e8a012324 2016-12-16) + // rustc 1.15.0-beta.5 (10893a9a3 2017-01-19) + // rustc 1.16.0-nightly (df8debf6d 2017-01-25) + + let pieces: Vec<_> = s.split(&[' ', '(', ')'][..]) + .filter(|s| !s.trim().is_empty()) + .collect(); + if pieces.len() != 4 { + return Err(human(format!( + "rust_version `{}` not recognized; \ + expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`", + s + ))); + } + + if pieces[1].contains("nightly") { + Ok(ChannelVersion::Nightly(time::strptime(pieces[3], "%Y-%m-%d")?.to_timespec())) + } else if pieces[1].contains("beta") { + Ok(ChannelVersion::Beta(time::strptime(pieces[3], "%Y-%m-%d")?.to_timespec())) + } else { + let v = semver::Version::parse(pieces[1])?; + if v.pre.is_empty() { + Ok(ChannelVersion::Stable(v)) + } else { + return Err(human(format!( + "rust_version `{}` not recognized as nightly, beta, or stable", + s + ))); + } + } + } +} + impl Version { pub fn find_by_num(conn: &GenericConnection, crate_id: i32, @@ -194,6 +238,31 @@ impl Version { &[&yanked, &self.id])); Ok(()) } + + pub fn store_build_info(&self, + conn: &GenericConnection, + info: upload::VersionBuildInfo, + krate: &Crate) -> CargoResult<()> { + + // Verify specified Rust version will parse before doing any inserting + info.channel_version()?; + + // Future improvement: allow overwriting an existing row, in which case + // the ON CONFLICT DO should set `passed` instead of doing nothing. + let inserted = conn.execute("INSERT INTO build_info \ + (version_id, rust_version, target, passed) \ + VALUES ($1, $2, $3, $4) \ + ON CONFLICT (version_id, rust_version, target) DO NOTHING", + &[&self.id, &info.rust_version, &info.target, &info.passed])?; + + if inserted == 0 { + return Err(human(format!( + "Build info already specified for {} v{} with {} and target {}", + krate.name, self.num, info.rust_version, info.target))); + } + + Ok(()) + } } impl Model for Version { @@ -371,3 +440,26 @@ fn modify_yank(req: &mut Request, yanked: bool) -> CargoResult { struct R { ok: bool } Ok(req.json(&R{ ok: true })) } + +/// Handles the `POST /crates/:crate_id/:version/build_info` route. +pub fn publish_build_info(req: &mut Request) -> CargoResult { + let mut body = String::new(); + try!(req.body().read_to_string(&mut body)); + let info: upload::VersionBuildInfo = try!(json::decode(&body).map_err(|e| { + human(format!("invalid upload request: {:?}", e)) + })); + + let (version, krate) = try!(version_and_crate(req)); + let user = try!(req.user()); + let tx = try!(req.tx()); + let owners = try!(krate.owners(tx)); + if try!(rights(req.app(), &owners, &user)) < Rights::Publish { + return Err(human("must already be an owner to publish build info")) + } + + version.store_build_info(tx, info, &krate)?; + + #[derive(RustcEncodable)] + struct R { ok: bool } + Ok(req.json(&R{ ok: true })) +} From 5e8fb39d26bed9d43c2e7533481ae17957253abe Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 31 Jan 2017 19:43:28 -0500 Subject: [PATCH 2/5] Create an API route to return a version's build info --- src/lib.rs | 1 + src/tests/version.rs | 23 ++++++++++++- src/version.rs | 78 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bdc8df74a98..9015f037c33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,6 +101,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners)); api_router.delete("/crates/:crate_id/:version/yank", C(version::yank)); api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank)); + api_router.get("/crates/:crate_id/:version/build_info", C(version::build_info)); api_router.put("/crates/:crate_id/:version/build_info", C(version::publish_build_info)); api_router.get("/crates/:crate_id/reverse_dependencies", C(krate::reverse_dependencies)); api_router.get("/versions", C(version::index)); diff --git a/src/tests/version.rs b/src/tests/version.rs index ada1378872f..40f03b58b7d 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -5,7 +5,7 @@ use conduit::{Handler, Request, Method}; use semver; use cargo_registry::db::RequestTransaction; -use cargo_registry::version::{EncodableVersion, Version}; +use cargo_registry::version::{EncodableVersion, Version, EncodableBuildInfo}; #[derive(RustcDecodable)] struct VersionList { versions: Vec } @@ -118,6 +118,27 @@ fn publish_build_info() { .with_method(Method::Put) .with_body(body.as_bytes()))); assert!(::json::(&mut response).ok); + + let mut response = ok_resp!(middle.call(req.with_path( + "/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Get))); + + #[derive(RustcDecodable)] + struct R { build_info: EncodableBuildInfo } + + let json = ::json::(&mut response); + assert_eq!( + json.build_info.ordering.get("nightly"), + Some(&vec![String::from("2017-01-25T00:00:00Z")]) + ); + assert_eq!( + json.build_info.ordering.get("beta"), + Some(&vec![String::from("2017-01-20T00:00:00Z")]) + ); + assert_eq!( + json.build_info.ordering.get("stable"), + Some(&vec![String::from("1.13.0")]) + ); } #[test] diff --git a/src/version.rs b/src/version.rs index dbdf357c0e5..3c25adb5362 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,5 +1,5 @@ +use std::collections::{HashMap, BTreeSet}; use std::str::FromStr; -use std::collections::HashMap; use conduit::{Request, Response}; use conduit_router::RequestParams; @@ -59,6 +59,15 @@ pub struct VersionLinks { pub authors: String, } +#[derive(RustcEncodable, RustcDecodable, Default)] +pub struct EncodableBuildInfo { + id: i32, + pub ordering: HashMap>, + pub stable: HashMap>, + pub beta: HashMap>, + pub nightly: HashMap>, +} + pub enum ChannelVersion { Stable(semver::Version), Beta(Timespec), @@ -393,6 +402,73 @@ pub fn downloads(req: &mut Request) -> CargoResult { Ok(req.json(&R{ version_downloads: downloads })) } +/// Handles the `GET /crates/:crate_id/:version/build_info` route. +pub fn build_info(req: &mut Request) -> CargoResult { + let (version, _) = try!(version_and_crate(req)); + + let tx = try!(req.tx()); + + let stmt = try!(tx.prepare("SELECT * FROM build_info + WHERE version_id = $1")); + let rows = stmt.query(&[&version.id])?; + + let mut build_info = EncodableBuildInfo::default(); + build_info.id = version.id; + let mut stables = BTreeSet::new(); + let mut betas = BTreeSet::new(); + let mut nightlies = BTreeSet::new(); + + for row in &rows { + let rust_version: String = row.get("rust_version"); + let rust_version: ChannelVersion = rust_version.parse()?; + let target = row.get("target"); + let passed = row.get("passed"); + + match rust_version { + ChannelVersion::Stable(semver) => { + let key = semver.to_string(); + stables.insert(semver); + build_info.stable.entry(key) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + ChannelVersion::Beta(date) => { + let key = ::encode_time(date); + betas.insert(date); + build_info.beta.entry(key) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + ChannelVersion::Nightly(date) => { + let key = ::encode_time(date); + nightlies.insert(date); + build_info.nightly.entry(key) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + } + } + + build_info.ordering.insert( + String::from("stable"), + stables.into_iter().map(|s| s.to_string()).collect() + ); + + build_info.ordering.insert( + String::from("beta"), + betas.into_iter().map(::encode_time).collect() + ); + + build_info.ordering.insert( + String::from("nightly"), + nightlies.into_iter().map(::encode_time).collect() + ); + + #[derive(RustcEncodable)] + struct R { build_info: EncodableBuildInfo } + Ok(req.json(&R{ build_info: build_info })) +} + /// Handles the `GET /crates/:crate_id/:version/authors` route. pub fn authors(req: &mut Request) -> CargoResult { let (version, _) = try!(version_and_crate(req)); From 4d616ba0a7913fef5213ab23047bf13c349b6653 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 17:55:29 -0500 Subject: [PATCH 3/5] Display latest stable, beta, nightly results for 64 bit tier 1 targets On a particular crate version's page. --- app/helpers/format-build-result.js | 13 +++++++++ app/helpers/format-day.js | 8 ++++++ app/helpers/value-or-default.js | 7 +++++ app/models/build-info.js | 46 ++++++++++++++++++++++++++++++ app/models/version.js | 1 + app/styles/crate.scss | 19 ++++++++++++ app/templates/crate/version.hbs | 34 ++++++++++++++++++++++ src/version.rs | 2 ++ 8 files changed, 130 insertions(+) create mode 100644 app/helpers/format-build-result.js create mode 100644 app/helpers/format-day.js create mode 100644 app/helpers/value-or-default.js create mode 100644 app/models/build-info.js diff --git a/app/helpers/format-build-result.js b/app/helpers/format-build-result.js new file mode 100644 index 00000000000..948862ae99c --- /dev/null +++ b/app/helpers/format-build-result.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export function formatBuildResult(result) { + if (result === true) { + return 'Pass'; + } else if (result === false) { + return 'Fail'; + } else { + return null; + } +} + +export default Ember.Helper.helper(params => formatBuildResult(params[0])); diff --git a/app/helpers/format-day.js b/app/helpers/format-day.js new file mode 100644 index 00000000000..33c2d80b102 --- /dev/null +++ b/app/helpers/format-day.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; +import moment from 'moment'; + +export function formatDay(date) { + return date ? moment(date).utc().format('YYYY-MM-DD') : null; +} + +export default Ember.Helper.helper(params => formatDay(params[0])); diff --git a/app/helpers/value-or-default.js b/app/helpers/value-or-default.js new file mode 100644 index 00000000000..7dda85e77aa --- /dev/null +++ b/app/helpers/value-or-default.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export function valueOrDefault(value, default_value) { + return value ? value : default_value; +} + +export default Ember.Helper.helper(params => valueOrDefault(params[0], params[1])); diff --git a/app/models/build-info.js b/app/models/build-info.js new file mode 100644 index 00000000000..ebfb9367df9 --- /dev/null +++ b/app/models/build-info.js @@ -0,0 +1,46 @@ +import DS from 'ember-data'; +import Ember from 'ember'; + +const TIER1 = [ + ['x86_64-unknown-linux-gnu', 'Linux'], + ['x86_64-apple-darwin', 'macOS'], + ['x86_64-pc-windows-gnu', 'Windows (GNU)'], + ['x86_64-pc-windows-msvc', 'Windows (MSVC)'], +]; + +const last = name => ( + Ember.computed(name, function() { + const items = this.get(name); + return items[items.length - 1]; + }) +); + +export default DS.Model.extend({ + version: DS.belongsTo('version', { async: true }), + ordering: DS.attr(), + stable: DS.attr(), + beta: DS.attr(), + nightly: DS.attr(), + + has_any_info: Ember.computed('ordering', function() { + const ordering = this.get('ordering'); + const num_results = ordering.stable.length + ordering.nightly.length + ordering.beta.length; + return num_results > 0; + }), + + latest_stable: last('ordering.stable'), + latest_beta: last('ordering.beta'), + latest_nightly: last('ordering.nightly'), + tier1_results: Ember.computed('nightly', 'latest_nightly', 'beta', 'latest_beta', 'stable', 'latest_stable', function() { + const nightly_results = this.get('nightly')[this.get('latest_nightly')] || {}; + const beta_results = this.get('beta')[this.get('latest_beta')] || {}; + const stable_results = this.get('stable')[this.get('latest_stable')] || {}; + + return TIER1.map(([target, display]) => ({ + display_target: display, + nightly: nightly_results[target], + beta: beta_results[target], + stable: stable_results[target] + })); + }), +}); diff --git a/app/models/version.js b/app/models/version.js index 0876d3cdf89..53a7cdd62f9 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -12,6 +12,7 @@ export default DS.Model.extend({ async: false }), authors: DS.hasMany('users', { async: true }), + build_info: DS.belongsTo('build-info', { async: true }), dependencies: DS.hasMany('dependency', { async: true }), version_downloads: DS.hasMany('version-download', { async: true }), diff --git a/app/styles/crate.scss b/app/styles/crate.scss index 05e2b453280..88249a9872c 100644 --- a/app/styles/crate.scss +++ b/app/styles/crate.scss @@ -256,6 +256,25 @@ } } +#crate-build-info { + padding-bottom: 50px; + border-bottom: 5px solid $gray-border; + margin-bottom: 30px; + + .description { + margin-bottom: 30px; + } + + table { + border: 1px solid $gray-border; + td, th { + border: 1px solid $gray-border; + padding: 5px 10px; + text-align: left; + } + } +} + #crate-downloads { @include display-flex; @include flex-wrap(wrap); diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 30dccfaf25f..cdf5f443cd1 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -217,6 +217,40 @@ +
+

Build info

+ + {{#if currentVersion.build_info.has_any_info }} +
+ Most recent reported build information for + version {{ currentVersion.num }}: +
+ + + + + + + + + + {{#each currentVersion.build_info.tier1_results as |result|}} + + + + + + + {{/each}} +
Tier 1 TargetStable: {{ value-or-default currentVersion.build_info.latest_stable 'None' }}Beta: {{ value-or-default (format-day currentVersion.build_info.latest_beta) 'None' }}Nightly: {{ value-or-default (format-day currentVersion.build_info.latest_nightly) 'None' }}
{{ result.display_target }}{{ format-build-result result.stable }}{{ format-build-result result.beta }}{{ format-build-result result.nightly }}
+ {{else}} +
+ No build information available for this version. If you are the + crate owner... // TODO LINK TO DOCS +
+ {{/if}} +
+

Stats Overview

diff --git a/src/version.rs b/src/version.rs index 3c25adb5362..bba4e3d2ee4 100644 --- a/src/version.rs +++ b/src/version.rs @@ -57,6 +57,7 @@ pub struct VersionLinks { pub dependencies: String, pub version_downloads: String, pub authors: String, + pub build_info: String, } #[derive(RustcEncodable, RustcDecodable, Default)] @@ -170,6 +171,7 @@ impl Version { version_downloads: format!("/api/v1/crates/{}/{}/downloads", crate_name, num), authors: format!("/api/v1/crates/{}/{}/authors", crate_name, num), + build_info: format!("/api/v1/crates/{}/{}/build_info", crate_name, num), }, } } From c1f79bfcf45faa9daf3f7a5f0c0a9c960f70ca04 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 17:58:04 -0500 Subject: [PATCH 4/5] Cache max stable, beta, nightly passing builds on crate record For the latest crate version only, for the purpose of showing a badge on crate list pages. --- src/bin/migrate.rs | 10 ++ src/krate.rs | 33 +++++- src/tests/all.rs | 3 + src/tests/version.rs | 236 +++++++++++++++++++++++++++++++++++++++++++ src/version.rs | 43 +++++++- 5 files changed, 321 insertions(+), 4 deletions(-) diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index df529c1e4a4..07e14140bbf 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -846,6 +846,16 @@ fn migrations() -> Vec { try!(tx.execute("DROP INDEX build_info_combo", &[])); Ok(()) }), + Migration::run(20170127143020, + "ALTER TABLE crates \ + ADD COLUMN max_build_info_stable VARCHAR, \ + ADD COLUMN max_build_info_beta TIMESTAMP, \ + ADD COLUMN max_build_info_nightly TIMESTAMP", + "ALTER TABLE crates \ + DROP COLUMN max_build_info_stable, \ + DROP COLUMN max_build_info_beta, \ + DROP COLUMN max_build_info_nightly", + ), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` diff --git a/src/krate.rs b/src/krate.rs index f0636beda4a..2d7bf740791 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -52,6 +52,9 @@ pub struct Crate { pub license: Option, pub repository: Option, pub max_upload_size: Option, + pub max_build_info_stable: Option, + pub max_build_info_beta: Option, + pub max_build_info_nightly: Option, } #[derive(RustcEncodable, RustcDecodable)] @@ -246,6 +249,7 @@ impl Crate { name, created_at, updated_at, downloads, max_version, description, homepage, documentation, license, repository, readme: _, id: _, max_upload_size: _, + max_build_info_stable: _, max_build_info_beta: _, max_build_info_nightly: _, } = self; let versions_link = match versions { Some(..) => None, @@ -386,11 +390,28 @@ impl Crate { let zero = semver::Version::parse("0.0.0").unwrap(); if *ver > self.max_version || self.max_version == zero { self.max_version = ver.clone(); + self.max_build_info_stable = None; + self.max_build_info_beta = None; + self.max_build_info_nightly = None; } - let stmt = try!(conn.prepare("UPDATE crates SET max_version = $1 - WHERE id = $2 RETURNING updated_at")); - let rows = try!(stmt.query(&[&self.max_version.to_string(), &self.id])); + + let stmt = conn.prepare(" \ + UPDATE crates \ + SET max_version = $1, + max_build_info_stable = $2, + max_build_info_beta = $3, + max_build_info_nightly = $4 \ + WHERE id = $5 \ + RETURNING updated_at")?; + let rows = try!(stmt.query(&[ + &self.max_version.to_string(), + &self.max_build_info_stable.as_ref().map(|vers| vers.to_string()), + &self.max_build_info_beta, + &self.max_build_info_nightly, + &self.id + ])); self.updated_at = rows.get(0).get("updated_at"); + Version::insert(conn, self.id, ver, features, authors) } @@ -460,6 +481,7 @@ impl Crate { impl Model for Crate { fn from_row(row: &Row) -> Crate { let max: String = row.get("max_version"); + let max_build_info_stable: Option = row.get("max_build_info_stable"); Crate { id: row.get("id"), name: row.get("name"), @@ -474,6 +496,11 @@ impl Model for Crate { license: row.get("license"), repository: row.get("repository"), max_upload_size: row.get("max_upload_size"), + max_build_info_stable: max_build_info_stable.map(|stable| { + semver::Version::parse(&stable).unwrap() + }), + max_build_info_beta: row.get("max_build_info_beta"), + max_build_info_nightly: row.get("max_build_info_nightly"), } } fn table_name(_: Option) -> &'static str { "crates" } diff --git a/src/tests/all.rs b/src/tests/all.rs index a008de37d67..74a529ca706 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -194,6 +194,9 @@ fn krate(name: &str) -> Crate { created_at: time::now().to_timespec(), downloads: 10, max_version: semver::Version::parse("0.0.0").unwrap(), + max_build_info_stable: None, + max_build_info_beta: None, + max_build_info_nightly: None, documentation: None, homepage: None, description: None, diff --git a/src/tests/version.rs b/src/tests/version.rs index 40f03b58b7d..e40f51ae561 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -3,9 +3,12 @@ use rustc_serialize::json::Json; use conduit::{Handler, Request, Method}; use semver; +use time; use cargo_registry::db::RequestTransaction; use cargo_registry::version::{EncodableVersion, Version, EncodableBuildInfo}; +use cargo_registry::upload; +use cargo_registry::krate::Crate; #[derive(RustcDecodable)] struct VersionList { versions: Vec } @@ -16,6 +19,10 @@ fn sv(s: &str) -> semver::Version { semver::Version::parse(s).unwrap() } +fn ts(s: &str) -> time::Timespec { + time::strptime(s, "%Y-%m-%d").expect("Bad date string").to_timespec() +} + #[test] fn index() { let (_b, app, middle) = ::app(); @@ -183,3 +190,232 @@ fn bad_rust_version_publish_build_info() { "rust_version `1.15.0` not recognized; \ expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`"); } + +#[test] +fn no_existing_max_build_info() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/versions"); + ::mock_user(&mut req, ::user("foo")); + let (krate, version) = ::mock_crate(&mut req, ::krate("no-existing-max")); + let req: &mut Request = &mut req; + let tx = req.tx().unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let krate = Crate::find_by_name(tx, "no-existing-max").unwrap(); + assert_eq!(krate.max_build_info_stable, Some(sv("1.14.0"))); + assert_eq!(krate.max_build_info_beta, Some(ts("2017-01-19"))); + assert_eq!(krate.max_build_info_nightly, Some(ts("2017-01-25"))); +} + +#[test] +fn failed_build_info_doesnt_update_max() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/versions"); + ::mock_user(&mut req, ::user("foo")); + let (krate, version) = ::mock_crate(&mut req, ::krate("failed-build")); + let req: &mut Request = &mut req; + let tx = req.tx().unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"), + target: String::from("anything"), + passed: false, // this is the different part + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"), + target: String::from("anything"), + passed: false, // this is the different part + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"), + target: String::from("anything"), + passed: false, // this is the different part + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let krate = Crate::find_by_name(tx, "failed-build").unwrap(); + assert_eq!(krate.max_build_info_stable, None); + assert_eq!(krate.max_build_info_beta, None); + assert_eq!(krate.max_build_info_nightly, None); +} + +#[test] +fn not_max_version_build_info_doesnt_update_max() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/versions"); + ::mock_user(&mut req, ::user("foo")); + let (krate, version) = ::mock_crate(&mut req, ::krate("old-rust-vers")); + let req: &mut Request = &mut req; + let tx = req.tx().unwrap(); + + // Setup: store build info for: + // stable 1.14.0, beta 2017-01-19, nightly 2017-01-25 + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + // Need to reload to see versions added in setup + let krate = Crate::find_by_name(tx, "old-rust-vers").unwrap(); + + // Report build info for: + // stable 1.13.0, beta 2017-01-01, nightly 2017-01-24 + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.13.0 (2c6933acc 2016-11-07)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.3 (beefcafe 2017-01-01)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (deadbeef 2017-01-24)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + // Max build info should still be 1.14.0, 2017-01-19, and 2017-01-25 + let krate = Crate::find_by_name(tx, "old-rust-vers").unwrap(); + assert_eq!(krate.max_build_info_stable, Some(sv("1.14.0"))); + assert_eq!(krate.max_build_info_beta, Some(ts("2017-01-19"))); + assert_eq!(krate.max_build_info_nightly, Some(ts("2017-01-25"))); +} + +#[test] +fn older_crate_version_in_build_info_doesnt_update_max() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/versions"); + ::mock_user(&mut req, ::user("foo")); + + let req: &mut Request = &mut req; + + let krate = ::krate("older-crate-version"); + + // Publish a version of the crate + let (krate, _) = ::mock_crate_vers(req, krate, &sv("2.0.0")); + + // Then go back and publish a lower version + let (krate, version) = ::mock_crate(req, krate); + + assert_eq!(krate.max_version, sv("2.0.0")); + assert_eq!(version.num, sv("1.0.0")); + + let tx = req.tx().unwrap(); + + // Publish the build info for the lower version + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let krate = Crate::find_by_name(tx, "older-crate-version").unwrap(); + assert_eq!(krate.max_build_info_stable, None); + assert_eq!(krate.max_build_info_beta, None); + assert_eq!(krate.max_build_info_nightly, None); +} + +#[test] +fn clear_max_build_info_on_new_crate_max_version() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/versions"); + ::mock_user(&mut req, ::user("foo")); + let (krate, version) = ::mock_crate(&mut req, ::krate("no-existing-max")); + let req: &mut Request = &mut req; + { + let tx = req.tx().unwrap(); + + // Setup: publish some build info for the current max version + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + + let info = upload::VersionBuildInfo { + rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"), + target: String::from("anything"), + passed: true, + }; + version.store_build_info(tx, info, &krate).unwrap(); + } + + // Then publish a higher version of the crate + ::mock_crate_vers(req, krate, &sv("2.0.0")); + + let tx = req.tx().unwrap(); + let krate = Crate::find_by_name(tx, "no-existing-max").unwrap(); + assert_eq!(krate.max_build_info_stable, None); + assert_eq!(krate.max_build_info_beta, None); + assert_eq!(krate.max_build_info_nightly, None); +} diff --git a/src/version.rs b/src/version.rs index bba4e3d2ee4..48335e65210 100644 --- a/src/version.rs +++ b/src/version.rs @@ -256,7 +256,7 @@ impl Version { krate: &Crate) -> CargoResult<()> { // Verify specified Rust version will parse before doing any inserting - info.channel_version()?; + let channel_version = info.channel_version()?; // Future improvement: allow overwriting an existing row, in which case // the ON CONFLICT DO should set `passed` instead of doing nothing. @@ -272,6 +272,47 @@ impl Version { krate.name, self.num, info.rust_version, info.target))); } + if info.passed && self.num == krate.max_version { + match channel_version { + ChannelVersion::Nightly(date) => { + let should_update = krate.max_build_info_nightly.as_ref().map(|max| { + date > *max + }).unwrap_or(true); + + if should_update { + conn.execute("UPDATE crates \ + SET max_build_info_nightly = $1 \ + WHERE id = $2", + &[&date, &krate.id])?; + } + }, + ChannelVersion::Beta(date) => { + let should_update = krate.max_build_info_beta.as_ref().map(|max| { + date > *max + }).unwrap_or(true); + + if should_update { + conn.execute("UPDATE crates \ + SET max_build_info_beta = $1 \ + WHERE id = $2", + &[&date, &krate.id])?; + } + }, + ChannelVersion::Stable(vers) => { + let should_update = krate.max_build_info_stable.as_ref().map(|max| { + vers > *max + }).unwrap_or(true); + + if should_update { + conn.execute("UPDATE crates \ + SET max_build_info_stable = $1 \ + WHERE id = $2", + &[&vers.to_string(), &krate.id])?; + } + }, + } + } + Ok(()) } } From 487e8b73089ddb9186cd7b27d63747e7b53be624 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 18:01:19 -0500 Subject: [PATCH 5/5] Show badge for latest crate version build success status On crate list pages. If the latest version builds on any stable version, show that it builds on stable. If not and it builds on any nightly version, show that. Otherwise, don't show any badge. --- app/components/badge-build-info.js | 41 +++++++++++++++++++ app/models/crate.js | 3 ++ app/templates/components/badge-build-info.hbs | 6 +++ app/templates/components/crate-row.hbs | 1 + src/krate.rs | 8 +++- 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 app/components/badge-build-info.js create mode 100644 app/templates/components/badge-build-info.hbs diff --git a/app/components/badge-build-info.js b/app/components/badge-build-info.js new file mode 100644 index 00000000000..2fe3efbaef6 --- /dev/null +++ b/app/components/badge-build-info.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; + +import { formatDay } from 'cargo/helpers/format-day'; + +export default Ember.Component.extend({ + tagName: 'span', + classNames: ['build_info'], + + build_info: Ember.computed('crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('crate.max_build_info_stable')) { + return 'stable'; + } else if (this.get('crate.max_build_info_beta')) { + return 'beta'; + } else if (this.get('crate.max_build_info_nightly')) { + return 'nightly'; + } else { + return null; + } + }), + color: Ember.computed('build_info', function() { + if (this.get('build_info') === 'stable') { + return 'brightgreen'; + } else if (this.get('build_info') === 'beta') { + return 'yellow'; + } else { + return 'orange'; + } + }), + version_display: Ember.computed('build_info', 'crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('build_info') === 'stable') { + return this.get('crate.max_build_info_stable'); + } else if (this.get('build_info') === 'beta') { + return formatDay(this.get('crate.max_build_info_beta')); + } else { + return formatDay(this.get('crate.max_build_info_nightly')); + } + }), + version_for_shields: Ember.computed('version_display', function() { + return this.get('version_display').replace(/-/g, '--'); + }), +}); diff --git a/app/models/crate.js b/app/models/crate.js index e5c07f40af8..ceb5cf106b4 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -16,6 +16,9 @@ export default DS.Model.extend({ documentation: DS.attr('string'), repository: DS.attr('string'), license: DS.attr('string'), + max_build_info_nightly: DS.attr('date'), + max_build_info_beta: DS.attr('date'), + max_build_info_stable: DS.attr('string'), versions: DS.hasMany('versions', { async: true }), badges: DS.attr(), diff --git a/app/templates/components/badge-build-info.hbs b/app/templates/components/badge-build-info.hbs new file mode 100644 index 00000000000..8d51aedd7e8 --- /dev/null +++ b/app/templates/components/badge-build-info.hbs @@ -0,0 +1,6 @@ +{{#if build_info}} +Known to build on {{ build_info }} {{ version_display }} +{{/if}} \ No newline at end of file diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs index 80c27e3e65e..d277fb93e10 100644 --- a/app/templates/components/crate-row.hbs +++ b/app/templates/components/crate-row.hbs @@ -10,6 +10,7 @@ {{#each crate.annotated_badges as |badge|}} {{component badge.component_name badge=badge}} {{/each}} + {{badge-build-info crate=crate}}
diff --git a/src/krate.rs b/src/krate.rs index 2d7bf740791..32a3262277b 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -69,6 +69,9 @@ pub struct EncodableCrate { pub created_at: String, pub downloads: i32, pub max_version: String, + pub max_build_info_stable: Option, + pub max_build_info_beta: Option, + pub max_build_info_nightly: Option, pub description: Option, pub homepage: Option, pub documentation: Option, @@ -249,7 +252,7 @@ impl Crate { name, created_at, updated_at, downloads, max_version, description, homepage, documentation, license, repository, readme: _, id: _, max_upload_size: _, - max_build_info_stable: _, max_build_info_beta: _, max_build_info_nightly: _, + max_build_info_stable, max_build_info_beta, max_build_info_nightly, } = self; let versions_link = match versions { Some(..) => None, @@ -271,6 +274,9 @@ impl Crate { categories: category_ids, badges: badges, max_version: max_version.to_string(), + max_build_info_stable: max_build_info_stable.map(|s| s.to_string()), + max_build_info_beta: max_build_info_beta.map(::encode_time), + max_build_info_nightly: max_build_info_nightly.map(::encode_time), documentation: documentation, homepage: homepage, description: description,