From 1211c5d07b05925fd9e199421bba3894c1e70b9c Mon Sep 17 00:00:00 2001
From: Giles Cope <gilescope@gmail.com>
Date: Fri, 22 Sep 2017 08:47:26 +0100
Subject: [PATCH 1/3] Implementation of Teamcity service messages to allow easy
 integration of Rust tests into TeamCity.

---
 src/libtest/lib.rs | 135 ++++++++++++++++++++++++++++++++++-----------
 1 file changed, 102 insertions(+), 33 deletions(-)

diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs
index 642eb28556408..fae07e9ce4371 100644
--- a/src/libtest/lib.rs
+++ b/src/libtest/lib.rs
@@ -258,12 +258,14 @@ impl Clone for MetricMap {
 #[derive(Copy, Clone, Debug)]
 pub struct Options {
     display_output: bool,
+    display_service_messages: bool
 }
 
 impl Options {
     pub fn new() -> Options {
         Options {
             display_output: false,
+            display_service_messages: false
         }
     }
 
@@ -385,6 +387,7 @@ fn optgroups() -> getopts::Options {
                                 of stdout", "PATH")
         .optflag("", "nocapture", "don't capture stdout/stderr of each \
                                    task, allow printing directly")
+        .optflag("", "teamcity", "Print Teamcity Service Messages to stdout to track test history.")
         .optopt("", "test-threads", "Number of threads used for running tests \
                                      in parallel", "n_threads")
         .optmulti("", "skip", "Skip tests whose names contain FILTER (this flag can \
@@ -468,6 +471,15 @@ pub fn parse_opts(args: &[String]) -> Option<OptRes> {
         };
     }
 
+    let mut teamcity = matches.opt_present("teamcity");
+    println!("teamcity set to {}", teamcity);
+    if !teamcity {
+        teamcity = match env::var("RUST_TEST_TEAMCITY_SERVICE_MESSAGES") {
+            Ok(val) => &val != "0",
+            Err(_) => false
+        };
+    }
+    println!("teamcity set to {}", teamcity);
     let test_threads = match matches.opt_str("test-threads") {
         Some(n_str) =>
             match n_str.parse::<usize>() {
@@ -507,8 +519,12 @@ pub fn parse_opts(args: &[String]) -> Option<OptRes> {
         quiet,
         test_threads,
         skip: matches.opt_strs("skip"),
-        options: Options::new(),
+        options: Options{
+            display_output: false,
+            display_service_messages: teamcity
+        },
     };
+   // test_opts.options.display_service_messages = teamcity;
 
     Some(Ok(test_opts))
 }
@@ -587,16 +603,31 @@ impl<T: Write> ConsoleTestState<T> {
         })
     }
 
-    pub fn write_ok(&mut self) -> io::Result<()> {
-        self.write_short_result("ok", ".", term::color::GREEN)
+    pub fn write_ok(&mut self, test: &TestDesc) -> io::Result<()> {
+        self.write_short_result("ok", ".", term::color::GREEN)?;
+
+        let name : String = service_message_escape(test.name.as_slice());
+        if self.options.display_service_messages {
+            self.write_plain(&format!("\n##teamcity[testFinished name='{}']\n", name))?;
+        }
+        std::result::Result::Ok(())
     }
 
-    pub fn write_failed(&mut self) -> io::Result<()> {
-        self.write_short_result("FAILED", "F", term::color::RED)
+    pub fn write_failed(&mut self, test: &TestDesc) -> io::Result<()> {
+
+        self.write_short_result("FAILED", "F", term::color::RED)?;
+        if self.options.display_service_messages {
+            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n", service_message_escape(test.name.as_slice())))?;
+        }
+        std::result::Result::Ok(())
     }
 
-    pub fn write_ignored(&mut self) -> io::Result<()> {
-        self.write_short_result("ignored", "i", term::color::YELLOW)
+    pub fn write_ignored(&mut self, test: &TestDesc) -> io::Result<()> {
+        self.write_short_result("ignored", "i", term::color::YELLOW)?;
+        if self.options.display_service_messages {
+            self.write_plain(&format!("\n##teamcity[testIgnored name='{}']\n", service_message_escape(test.name.as_slice())))?;
+        }
+        std::result::Result::Ok(())
     }
 
     pub fn write_allowed_fail(&mut self) -> io::Result<()> {
@@ -669,15 +700,24 @@ impl<T: Write> ConsoleTestState<T> {
             Ok(())
         } else {
             let name = test.padded_name(self.max_name_len, align);
+            if self.options.display_service_messages {
+                let use_std_out = if stdout_isatty() {
+                    "true"
+                } else {
+                    "false"
+                };
+                self.write_plain(&format!("\n##teamcity[testStarted name='{}' captureStandardOutput='{}']\n",
+                                          service_message_escape(test.name.as_slice()), use_std_out))?;
+            }
             self.write_plain(&format!("test {} ... ", name))
         }
     }
 
-    pub fn write_result(&mut self, result: &TestResult) -> io::Result<()> {
+    pub fn write_result(&mut self, test: &TestDesc, result: &TestResult) -> io::Result<()> {
         match *result {
-            TrOk => self.write_ok(),
-            TrFailed | TrFailedMsg(_) => self.write_failed(),
-            TrIgnored => self.write_ignored(),
+            TrOk => self.write_ok(&test),
+            TrFailed | TrFailedMsg(_) => self.write_failed(&test),
+            TrIgnored => self.write_ignored(&test),
             TrAllowedFail => self.write_allowed_fail(),
             TrMetrics(ref mm) => {
                 self.write_metric()?;
@@ -685,12 +725,16 @@ impl<T: Write> ConsoleTestState<T> {
             }
             TrBench(ref bs) => {
                 self.write_bench()?;
-                self.write_plain(&format!(": {}\n", fmt_bench_samples(bs)))
+                let msg = self.fmt_bench_samples(test, bs).clone();
+                self.write_plain(&format!(": {}\n", msg))
             }
         }
     }
 
     pub fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> {
+        if self.options.display_service_messages {
+            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n", service_message_escape(desc.name.as_slice())))?;
+        }
         self.write_plain(&format!("test {} has been running for over {} seconds\n",
                                   desc.name,
                                   TEST_WARN_TIMEOUT_S))
@@ -705,8 +749,7 @@ impl<T: Write> ConsoleTestState<T> {
     }
 
     pub fn write_log_result(&mut self, test: &TestDesc, result: &TestResult) -> io::Result<()> {
-        self.write_log(
-            format!("{} {}\n",
+        let msg =format!("{} {}\n",
                     match *result {
                         TrOk => "ok".to_owned(),
                         TrFailed => "failed".to_owned(),
@@ -714,9 +757,40 @@ impl<T: Write> ConsoleTestState<T> {
                         TrIgnored => "ignored".to_owned(),
                         TrAllowedFail => "failed (allowed)".to_owned(),
                         TrMetrics(ref mm) => mm.fmt_metrics(),
-                        TrBench(ref bs) => fmt_bench_samples(bs),
+                        TrBench(ref bs) => self.fmt_bench_samples(test, bs),
                     },
-                    test.name))
+                    test.name);
+        self.write_log(msg)
+    }
+
+    pub fn fmt_bench_samples(&self, test: &TestDesc, bs: &BenchSamples) -> String {
+        use std::fmt::Write;
+        let mut output = String::new();
+
+        let median = bs.ns_iter_summ.median as usize;
+        let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize;
+
+        output.write_fmt(format_args!("{:>11} ns/iter (+/- {})",
+                                      fmt_thousands_sep(median, ','),
+                                      fmt_thousands_sep(deviation, ','))).unwrap();
+
+        if bs.mb_s != 0 {
+            output.write_fmt(format_args!(" = {} MB/s", bs.mb_s)).unwrap();
+
+            if self.options.display_service_messages {
+                output.write_fmt(format_args!("##teamcity[buildStatisticValue key='{}_MB_s' value='{}']\n",
+                                              test.name.as_slice(), bs.mb_s)).unwrap();
+            }
+        }
+        if self.options.display_service_messages {
+            output.write_fmt(format_args!("\n##teamcity[buildStatisticValue key='{}_ns_iter_median' value='{}']\n",
+                                          test.name.as_slice(), median)).unwrap();
+
+            output.write_fmt(format_args!("##teamcity[buildStatisticValue key='{}_std_dev' value='{}']\n",
+                                          test.name.as_slice(), deviation)).unwrap();
+        }
+        output.write_fmt(format_args!("##teamcity[testFinished name='{}']\n", test.name.as_slice())).unwrap();
+        output
     }
 
     pub fn write_failures(&mut self) -> io::Result<()> {
@@ -813,6 +887,17 @@ impl<T: Write> ConsoleTestState<T> {
     }
 }
 
+// Follow TeamCity escaping rules for service messages.
+fn service_message_escape(message: &str) -> String {
+    message.replace('|', "||")
+        .replace('\'', "|'")
+        .replace('\n', "|n")
+        .replace('\r', "|r")
+        .replace('[', "|[")
+        .replace(']', "|]")
+    //TODO \uNNNN (unicode symbol with code 0xNNNN) to "|0xNNNN'
+}
+
 // Format a number with thousands separators
 fn fmt_thousands_sep(mut n: usize, sep: char) -> String {
     use std::fmt::Write;
@@ -837,22 +922,6 @@ fn fmt_thousands_sep(mut n: usize, sep: char) -> String {
     output
 }
 
-pub fn fmt_bench_samples(bs: &BenchSamples) -> String {
-    use std::fmt::Write;
-    let mut output = String::new();
-
-    let median = bs.ns_iter_summ.median as usize;
-    let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize;
-
-    output.write_fmt(format_args!("{:>11} ns/iter (+/- {})",
-                                  fmt_thousands_sep(median, ','),
-                                  fmt_thousands_sep(deviation, ',')))
-          .unwrap();
-    if bs.mb_s != 0 {
-        output.write_fmt(format_args!(" = {} MB/s", bs.mb_s)).unwrap();
-    }
-    output
-}
 
 // List the tests to console, and optionally to logfile. Filters are honored.
 pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> {
@@ -908,7 +977,7 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
             TeTimeout(ref test) => st.write_timeout(test),
             TeResult(test, result, stdout) => {
                 st.write_log_result(&test, &result)?;
-                st.write_result(&result)?;
+                st.write_result(&test, &result)?;
                 match result {
                     TrOk => {
                         st.passed += 1;

From 6ce755210843eba05f66f9703e47d6303a024760 Mon Sep 17 00:00:00 2001
From: Giles Cope <gilescope@gmail.com>
Date: Sun, 24 Sep 2017 17:33:46 +0100
Subject: [PATCH 2/3] Added TeamCity service messages

---
 src/libtest/lib.rs | 81 ++++++++++++++++++++++++++++++++--------------
 1 file changed, 56 insertions(+), 25 deletions(-)

diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs
index fae07e9ce4371..3e936ff63207e 100644
--- a/src/libtest/lib.rs
+++ b/src/libtest/lib.rs
@@ -278,12 +278,11 @@ impl Options {
 // The default console test runner. It accepts the command line
 // arguments and a vector of test_descs.
 pub fn test_main(args: &[String], tests: Vec<TestDescAndFn>, options: Options) {
-    let mut opts = match parse_opts(args) {
+    let opts = match parse_opts(args, options) {
         Some(Ok(o)) => o,
         Some(Err(msg)) => panic!("{:?}", msg),
         None => return,
     };
-    opts.options = options;
     if opts.list {
         if let Err(e) = list_tests_console(&opts, tests) {
             panic!("io error when listing tests: {:?}", e);
@@ -434,7 +433,7 @@ Test Attributes:
 }
 
 // Parses command line arguments into test options
-pub fn parse_opts(args: &[String]) -> Option<OptRes> {
+pub fn parse_opts(args: &[String], mut options: Options) -> Option<OptRes> {
     let opts = optgroups();
     let matches = match opts.parse(&args[1..]) {
         Ok(m) => m,
@@ -506,6 +505,10 @@ pub fn parse_opts(args: &[String]) -> Option<OptRes> {
         }
     };
 
+    if teamcity {
+        options.display_service_messages = true
+    }
+
     let test_opts = TestOpts {
         list,
         filter,
@@ -519,12 +522,8 @@ pub fn parse_opts(args: &[String]) -> Option<OptRes> {
         quiet,
         test_threads,
         skip: matches.opt_strs("skip"),
-        options: Options{
-            display_output: false,
-            display_service_messages: teamcity
-        },
+        options: options
     };
-   // test_opts.options.display_service_messages = teamcity;
 
     Some(Ok(test_opts))
 }
@@ -607,6 +606,7 @@ impl<T: Write> ConsoleTestState<T> {
         self.write_short_result("ok", ".", term::color::GREEN)?;
 
         let name : String = service_message_escape(test.name.as_slice());
+        println!("display_service_messages {}", &self.options.display_service_messages);
         if self.options.display_service_messages {
             self.write_plain(&format!("\n##teamcity[testFinished name='{}']\n", name))?;
         }
@@ -617,7 +617,8 @@ impl<T: Write> ConsoleTestState<T> {
 
         self.write_short_result("FAILED", "F", term::color::RED)?;
         if self.options.display_service_messages {
-            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n", service_message_escape(test.name.as_slice())))?;
+            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n",
+                                      service_message_escape(test.name.as_slice())))?;
         }
         std::result::Result::Ok(())
     }
@@ -625,7 +626,8 @@ impl<T: Write> ConsoleTestState<T> {
     pub fn write_ignored(&mut self, test: &TestDesc) -> io::Result<()> {
         self.write_short_result("ignored", "i", term::color::YELLOW)?;
         if self.options.display_service_messages {
-            self.write_plain(&format!("\n##teamcity[testIgnored name='{}']\n", service_message_escape(test.name.as_slice())))?;
+            self.write_plain(&format!("\n##teamcity[testIgnored name='{}']\n",
+                                      service_message_escape(test.name.as_slice())))?;
         }
         std::result::Result::Ok(())
     }
@@ -706,8 +708,9 @@ impl<T: Write> ConsoleTestState<T> {
                 } else {
                     "false"
                 };
-                self.write_plain(&format!("\n##teamcity[testStarted name='{}' captureStandardOutput='{}']\n",
-                                          service_message_escape(test.name.as_slice()), use_std_out))?;
+                self.write_plain(
+                    &format!("\n##teamcity[testStarted name='{}' captureStandardOutput='{}']\n",
+                             service_message_escape(test.name.as_slice()), use_std_out))?;
             }
             self.write_plain(&format!("test {} ... ", name))
         }
@@ -733,7 +736,8 @@ impl<T: Write> ConsoleTestState<T> {
 
     pub fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> {
         if self.options.display_service_messages {
-            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n", service_message_escape(desc.name.as_slice())))?;
+            self.write_plain(&format!("\n##teamcity[testFailed name='{}']\n",
+                                      service_message_escape(desc.name.as_slice())))?;
         }
         self.write_plain(&format!("test {} has been running for over {} seconds\n",
                                   desc.name,
@@ -778,18 +782,24 @@ impl<T: Write> ConsoleTestState<T> {
             output.write_fmt(format_args!(" = {} MB/s", bs.mb_s)).unwrap();
 
             if self.options.display_service_messages {
-                output.write_fmt(format_args!("##teamcity[buildStatisticValue key='{}_MB_s' value='{}']\n",
-                                              test.name.as_slice(), bs.mb_s)).unwrap();
+                output.write_fmt(
+                    format_args!("##teamcity[buildStatisticValue key='{}_MB_s' value='{}']\n",
+                                 test.name.as_slice(), bs.mb_s)).unwrap();
             }
         }
         if self.options.display_service_messages {
-            output.write_fmt(format_args!("\n##teamcity[buildStatisticValue key='{}_ns_iter_median' value='{}']\n",
-                                          test.name.as_slice(), median)).unwrap();
-
-            output.write_fmt(format_args!("##teamcity[buildStatisticValue key='{}_std_dev' value='{}']\n",
-                                          test.name.as_slice(), deviation)).unwrap();
-        }
-        output.write_fmt(format_args!("##teamcity[testFinished name='{}']\n", test.name.as_slice())).unwrap();
+            output.write_fmt(
+                format_args!(
+                    "\n##teamcity[buildStatisticValue key='{}_ns_iter_median' value='{}']\n",
+                             test.name.as_slice(), median)).unwrap();
+
+            output.write_fmt(
+                format_args!("##teamcity[buildStatisticValue key='{}_std_dev' value='{}']\n",
+                             test.name.as_slice(), deviation)).unwrap();
+        }
+        output.write_fmt(
+            format_args!("##teamcity[testFinished name='{}']\n",
+                         test.name.as_slice())).unwrap();
         output
     }
 
@@ -895,7 +905,7 @@ fn service_message_escape(message: &str) -> String {
         .replace('\r', "|r")
         .replace('[', "|[")
         .replace(']', "|]")
-    //TODO \uNNNN (unicode symbol with code 0xNNNN) to "|0xNNNN'
+    //FIXME \uNNNN (unicode symbol with code 0xNNNN) to "|0xNNNN'
 }
 
 // Format a number with thousands separators
@@ -1029,6 +1039,7 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
         let n = t.desc.name.as_slice();
         st.max_name_len = n.len();
     }
+    println!("opts are {}", opts.options.display_service_messages);
     run_tests(opts, tests, |x| callback(&x, &mut st))?;
     return st.write_run_finish();
 }
@@ -1919,11 +1930,31 @@ mod tests {
     #[test]
     fn parse_ignored_flag() {
         let args = vec!["progname".to_string(), "filter".to_string(), "--ignored".to_string()];
-        let opts = match parse_opts(&args) {
+        let opts = match parse_opts(&args, TestOpts::new().options) {
+            Some(Ok(o)) => o,
+            _ => panic!("Malformed arg in parse_ignored_flag"),
+        };
+        assert!(opts.run_ignored);
+    }
+
+    #[test]
+    fn parse_teamcity_flag() {
+        let args = vec!["progname".to_string(), "filter".to_string(), "--teamcity".to_string()];
+        let opts = match parse_opts(&args, TestOpts::new().options) {
+            Some(Ok(o)) => o,
+            _ => panic!("Malformed arg in parse_ignored_flag"),
+        };
+        assert!(opts.options.display_service_messages);
+    }
+
+    #[test]
+    fn teamcity_service_messages_defaults_to_off() {
+        let args = vec!["progname".to_string(), "filter".to_string(), "--ignored".to_string()];
+        let opts = match parse_opts(&args, TestOpts::new().options) {
             Some(Ok(o)) => o,
             _ => panic!("Malformed arg in parse_ignored_flag"),
         };
-        assert!((opts.run_ignored));
+        assert!(!opts.options.display_service_messages);
     }
 
     #[test]

From 05db1cb351da157d22376d2933222745850f5f5a Mon Sep 17 00:00:00 2001
From: Giles Cope <gilescope@gmail.com>
Date: Sun, 24 Sep 2017 19:15:43 +0100
Subject: [PATCH 3/3] Removing debugging statements.

---
 src/libtest/lib.rs | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs
index 3e936ff63207e..cdf881b8caefb 100644
--- a/src/libtest/lib.rs
+++ b/src/libtest/lib.rs
@@ -471,14 +471,13 @@ pub fn parse_opts(args: &[String], mut options: Options) -> Option<OptRes> {
     }
 
     let mut teamcity = matches.opt_present("teamcity");
-    println!("teamcity set to {}", teamcity);
     if !teamcity {
         teamcity = match env::var("RUST_TEST_TEAMCITY_SERVICE_MESSAGES") {
             Ok(val) => &val != "0",
             Err(_) => false
         };
     }
-    println!("teamcity set to {}", teamcity);
+
     let test_threads = match matches.opt_str("test-threads") {
         Some(n_str) =>
             match n_str.parse::<usize>() {
@@ -606,7 +605,6 @@ impl<T: Write> ConsoleTestState<T> {
         self.write_short_result("ok", ".", term::color::GREEN)?;
 
         let name : String = service_message_escape(test.name.as_slice());
-        println!("display_service_messages {}", &self.options.display_service_messages);
         if self.options.display_service_messages {
             self.write_plain(&format!("\n##teamcity[testFinished name='{}']\n", name))?;
         }
@@ -1039,7 +1037,6 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
         let n = t.desc.name.as_slice();
         st.max_name_len = n.len();
     }
-    println!("opts are {}", opts.options.display_service_messages);
     run_tests(opts, tests, |x| callback(&x, &mut st))?;
     return st.write_run_finish();
 }