non_std 0.1.4

Collection of general purpose tools for solving problems. Fundamentally extend the language without spoiling, so may be used solely or in conjunction with another module of such kind.
Documentation
use std::env::current_dir;
use std::path::PathBuf;
use wtools::error::BasicError;
use wca::instruction::Instruction;
use ::wpublisher::manifest::Manifest;

///
/// Perform smoke testing.
///

pub fn smoke( instruction : &Instruction ) -> Result< (), BasicError >
{
  let mut current_path = current_dir().unwrap();

  let subject_path = PathBuf::from( &instruction.subject );
  let module_path = if subject_path.is_relative()
  {
    current_path.push( &instruction.subject );
    current_path
  }
  else
  {
    subject_path
  };

  let mut manifest_path = module_path.clone();
  manifest_path.push( "Cargo.toml" );

  if !manifest_path.exists()
  {
    let msg = format!( "Current directory {:?} has no file \"Cargo.toml\"", module_path.canonicalize().unwrap() );
    return Err( BasicError::new( msg ) );
  }

  let mut manifest = Manifest::new();
  manifest.manifest_path_from_str( &manifest_path ).unwrap();
  manifest.load().unwrap();
  let data = manifest.manifest_data.as_deref().unwrap();

  /* */

  let module_name = &data[ "package" ][ "name" ].clone();
  let module_name = module_name.as_str().unwrap();

  let code_path = match instruction.properties_map.get( "code_path" )
  {
    Some( x ) => PathBuf::from( x.clone().primitive().unwrap() ),
    None => PathBuf::default(),
  };

  let mut data = None;
  if code_path.exists()
  {
    data = Some( std::fs::read_to_string( code_path ).unwrap() );
  }

  let version = match instruction.properties_map.get( "version" )
  {
    Some( x ) => x.clone().primitive().unwrap(),
    None => "*".to_string(),
  };

  let smoke = match instruction.properties_map.get( "smoke" )
  {
    Some( x ) => x.clone().primitive().unwrap(),
    None =>
    {
      if let Ok( x ) = std::env::var( "WITH_SMOKE" )
      {
        x
      }
      else
      {
        "local".to_string()
      }
    },
  };

  /* */

  if smoke != "false" && smoke != "0"
  {
    let mut threads = vec![];
    if smoke == "local" || smoke != "published"
    {
      let module_name = module_name.clone().to_owned();
      let data = data.clone();
      let version = version.clone();
      let thread = std::thread::spawn( move ||
      {
        let mut t = SmokeModuleTest::new( module_name );
        t.test_postfix( "_test_local" );
        if data.is_some()
        {
          t.code( data.as_ref().unwrap() );
        }
        t.version( version.as_str() );
        t.local_path_clause( module_path.to_str().unwrap() );

        t.clean( true ).unwrap();
        t.form().unwrap();
        t.perform().unwrap();
        t.clean( false ).unwrap();
      });
      threads.push( thread );
    }

    if smoke == "published" || smoke != "local"
    {
      let module_name = module_name.clone().to_owned();
      let data = data.clone();
      let version = version.clone();
      let thread = std::thread::spawn( move ||
      {
        let mut t = SmokeModuleTest::new( module_name );
        t.test_postfix( "_test_published" );
        if data.is_some()
        {
          t.code( data.as_ref().unwrap() );
        }
        t.version( version.as_str() );

        t.clean( true ).unwrap();
        t.form().unwrap();
        t.perform().unwrap();
        t.clean( false ).unwrap();
      });
      threads.push( thread );
    }

    for thread in threads
    {
      thread.join().unwrap();
    }
  }

  Ok( () )
}

//

#[ derive( Debug ) ]
struct SmokeModuleTest< 'a >
{
  pub dependency_name : String,
  pub version : &'a str,
  pub local_path_clause : &'a str,
  pub code : String,
  pub test_path : std::path::PathBuf,
  pub test_postfix : &'a str,
}

impl< 'a > SmokeModuleTest< 'a >
{
  fn new( dependency_name : String ) -> SmokeModuleTest< 'a >
  {
    let test_postfix = "_smoke_test";
    let smoke_test_path = format!( "{}{}", dependency_name, test_postfix );
    let mut test_path = std::env::temp_dir();
    test_path.push( smoke_test_path );

    SmokeModuleTest
    {
      dependency_name,
      version : "*",
      local_path_clause : "",
      code : "".to_string(),
      test_path,
      test_postfix,
    }
  }

  fn version( &mut self, version : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.version = version.as_ref();
    self
  }

  fn local_path_clause( &mut self, local_path_clause : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.local_path_clause = local_path_clause;
    self
  }

  fn test_postfix( &mut self, test_postfix : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.test_postfix = test_postfix;
    let smoke_test_path = format!( "{}{}", self.dependency_name, test_postfix );
    self.test_path.pop();
    self.test_path.push( smoke_test_path );
    self
  }

  fn code( &mut self, code : impl AsRef< str > + 'a ) -> &mut SmokeModuleTest< 'a >
  {
    self.code = code.as_ref().into();
    self
  }

  fn form( &mut self ) -> Result< (), &'static str >
  {
    std::fs::create_dir( &self.test_path ).unwrap();

    let mut test_path = self.test_path.clone();

    /* create binary test module */
    let test_name = format!( "{}{}", self.dependency_name, self.test_postfix );
    let output = std::process::Command::new( "cargo" )
    .current_dir( &test_path )
    .args([ "new", "--bin", &test_name ])
    .output()
    .expect( "Failed to execute command" );
    println!( "Creating smoke binary module :\n\n{}", std::str::from_utf8( &output.stderr ).expect( "Found invalid UTF-8" ) );

    test_path.push( &test_name );

    /* setup config */
    #[ cfg( target_os = "windows" ) ]
    let local_path_clause = if self.local_path_clause == "" { "".to_string() } else { format!( ", path = \"{}\"", self.local_path_clause.escape_default() ) };
    #[ cfg( not( target_os = "windows" ) ) ]
    let local_path_clause = if self.local_path_clause == "" { "".to_string() } else { format!( ", path = \"{}\"", self.local_path_clause ) };
    let dependencies_section = format!( "{} = {{ version = \"{}\" {} }}", self.dependency_name, self.version, &local_path_clause );
    let config_data = format!
    (
      "[package]
      edition = \"2021\"
      name = \"{}_smoke_test\"
      version = \"0.0.1\"

      [dependencies]
      {}",
      &self.dependency_name,
      &dependencies_section
    );
    let mut config_path = test_path.clone();
    config_path.push( "Cargo.toml" );
    println!( "Manifest of module \"{}\" :\n\n      {}\n", test_name, config_data );
    std::fs::write( config_path, config_data ).unwrap();

    /* write code */
    test_path.push( "src" );
    test_path.push( "main.rs" );
    if self.code == ""
    {
      self.code = format!( "use ::{}::*;", self.dependency_name );
    }
    let code = format!
    (
      "#[ allow( unused_imports ) ]
      fn main()
      {{
        {}
      }}",
      self.code,
    );
    self.code = code;
    std::fs::write( &test_path, &self.code ).unwrap();

    Ok( () )
  }

  fn perform( &self ) -> Result<(), BasicError>
  {
    let mut test_path = self.test_path.clone();
    let test_name = format!( "{}{}", self.dependency_name, self.test_postfix );
    test_path.push( test_name );

    let output = std::process::Command::new( "cargo" )
    .current_dir( test_path )
    .args([ "run", "--release" ])
    .output()
    .unwrap();
    println!( "{}", std::str::from_utf8( &output.stdout ).expect( "Found invalid UTF-8" ) );
    println!( "{}", std::str::from_utf8( &output.stderr ).expect( "Found invalid UTF-8" ) );
    println!( "Process status :\n  {}\n", output.status );
    println!( "Code :\n\n      {}\n", self.code );

    if !output.status.success()
    {
      return Err( BasicError::new( "Smoke test failed" ) );
    }

    Ok( () )
  }

  fn clean( &self, force : bool ) -> Result<(), &'static str>
  {
    let result = std::fs::remove_dir_all( &self.test_path );
    if force
    {
      result.unwrap_or_default();
    }
    else
    {
      let msg = format!( "Cannot remove temporary directory {}. Please, remove it manually", &self.test_path.display() );
      result.expect( &msg );
    }
    Ok( () )
  }
}