<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Lab Grader</title>
<meta name="robots" content="noindex" />
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="introduction.html">Introduction</a></li><li class="chapter-item expanded "><a href="example_grader/home.html"><strong aria-hidden="true">1.</strong> Example Grader</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="example_grader/criteria.html"><strong aria-hidden="true">1.1.</strong> Defining the Criteria</a></li><li class="chapter-item expanded "><a href="example_grader/submission.html"><strong aria-hidden="true">1.2.</strong> Building a Submission</a></li><li class="chapter-item expanded "><a href="example_grader/rubric.html"><strong aria-hidden="true">1.3.</strong> Building the Rubric</a></li><li class="chapter-item expanded "><a href="example_grader/tests.html"><strong aria-hidden="true">1.4.</strong> Writing the Tests</a></li><li class="chapter-item expanded "><a href="example_grader/grade.html"><strong aria-hidden="true">1.5.</strong> Grading</a></li><li class="chapter-item expanded "><a href="example_grader/submit.html"><strong aria-hidden="true">1.6.</strong> Submitting</a></li><li class="chapter-item expanded "><a href="example_grader/results.html"><strong aria-hidden="true">1.7.</strong> The Results</a></li></ol></li><li class="chapter-item expanded "><a href="criteria/home.html"><strong aria-hidden="true">2.</strong> Criteria</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="criteria/yaml_spec.html"><strong aria-hidden="true">2.1.</strong> YAML Specification</a></li><li class="chapter-item expanded "><a href="criteria/criterion.html"><strong aria-hidden="true">2.2.</strong> A Single Criterion</a></li><li class="chapter-item expanded "><a href="criteria/test.html"><strong aria-hidden="true">2.3.</strong> A Criteria Test</a></li><li class="chapter-item expanded "><a href="criteria/rubric.html"><strong aria-hidden="true">2.4.</strong> A Rubric</a></li></ol></li><li class="chapter-item expanded "><a href="submission/home.html"><strong aria-hidden="true">3.</strong> Submission</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="submission/test_data.html"><strong aria-hidden="true">3.1.</strong> TestData</a></li><li class="chapter-item expanded "><a href="submission/prompt.html"><strong aria-hidden="true">3.2.</strong> The prompt! macro</a></li><li class="chapter-item expanded "><a href="submission/create.html"><strong aria-hidden="true">3.3.</strong> Creating a Submission</a></li><li class="chapter-item expanded "><a href="submission/submit.html"><strong aria-hidden="true">3.4.</strong> Submitting</a></li><li class="chapter-item expanded "><a href="submission/server.html"><strong aria-hidden="true">3.5.</strong> Submission Server</a></li></ol></li><li class="chapter-item expanded "><a href="helpers/home.html"><strong aria-hidden="true">4.</strong> Helpers</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="helpers/fs.html"><strong aria-hidden="true">4.1.</strong> File System</a></li><li class="chapter-item expanded "><a href="helpers/web.html"><strong aria-hidden="true">4.2.</strong> Web</a></li><li class="chapter-item expanded "><a href="helpers/cli.html"><strong aria-hidden="true">4.3.</strong> CLI</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Lab Grader</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1><a class="header" href="#introduction" id="introduction">Introduction</a></h1>
<p>This Rust crate automates a lot of the necessary steps for testing criteria, originally created to grade labs in technology management classes.</p>
<p>Detailed documentation can be <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/index.html">found here on docs.rs</a>.</p>
<blockquote>
<p>Note: this documentation is still very much a work in progress, and some sections are just missing. If you have any questions, send me an email at <code>llamicron@gmail.com</code> or on discord at <code>CoconutCake#3161</code>.</p>
</blockquote>
<p>Links:</p>
<ul>
<li><a href="https://docs.rs/lab_grader">API documentation</a></li>
<li><a href="https://github.com/llamicron/lab_grader">Repository</a></li>
</ul>
<h2><a class="header" href="#use-case" id="use-case">Use Case</a></h2>
<p>You should know that this program was intended for a technology management class. During the labs, the students would be managing Azure VMs, using software like Docker, Git and Github, etc. They were acting as SysAdmins, doing all the regular SysAdmin stuff. We needed a way to check each (60+) student's work on their VM or development machines, and report that back to us.</p>
<p>This poses a unique challenge. How do you get proof that every student has the right version of Python installed, or has a web server running on the right port? There's lots of different possibilities.</p>
<p>My original solution was to write a Rust program that checked everything and sent the results back to us. It wasn't modular or reusable; it was very much a bodge that was intended to work once. But after I wrote the fourth lab grader and prepared to write one to grade the final exam, I realized that I needed to set this in stone.</p>
<p>This library was the outcome. It's a glorified task-runner, but it's actually kind of cool. If you have any need of running a lot of commands, checking web sites and APIs, examining the file system, or performing anything across a lot of machines and getting a report from each one, then this library is for you.</p>
<h2><a class="header" href="#process" id="process">Process</a></h2>
<p>The general process for writing a grader is this</p>
<ul>
<li>
<p>Make a new rust project - you can do this with <code>cargo new</code></p>
</li>
<li>
<p>Define your criteria - You'll write all the details about each criteria in yaml. Things like name and description, point value, etc.</p>
</li>
<li>
<p>Build a Rubric - from within Rust, you'll load your yaml data into a <code>Rubric</code>. Then you'll write a function (a <em>test</em>) for each criterion and "attach" each test where it belongs.</p>
</li>
<li>
<p>Define a Submission - Any data you want sent back to you, as well as any data you may need inside the criteria tests should be stored in a submission.</p>
</li>
<li>
<p>Grade the Submission against the Rubric - this is done with one function call.</p>
</li>
<li>
<p>Submit the Submission - this is done by POSTing the data (with the web helper module). Again, one function call.</p>
</li>
</ul>
<p>That might seem like a lot, but it's pretty easy once you do it. A good place to get started is the example grader exercise. It will walk you through a complete grader.</p>
<h2><a class="header" href="#terminology" id="terminology">Terminology</a></h2>
<p>Here's a list of the terminology that I use throughout the program. This will help you understand exactly what's going on. It's pretty straightforward anyway but I'd rather be explicit about this.</p>
<p>If a term is formatted like <code>this</code>, that means it's represented in code as a struct or module of the same name.</p>
<ul>
<li>
<p>Grader - The program you're writing when you define criteria. "Grader" may not be the best term for your use case, but it's how I use this library.</p>
</li>
<li>
<p><code>Criterion</code> - a bit of clerical data, and a "test", which is just a function that returns true or false. The Criterion's test is the heart of the application. It's a function that you write. Data can be passed into the Criterions test, but the criterion itself doesn't store it.</p>
</li>
<li>
<p><code>Rubric</code> - a rubric is basically just a wrapper around a collection of criteria. This is the highest level of abstraction. You may want to run your tests in batches, which would be multiple rubrics. Each yaml file that you define later on is a single rubric. 1 yaml file = 1 Rubric.</p>
</li>
<li>
<p>Submission Server - just a web server that accepts Submissions as json data, then writes them to a file. You can start this server in one function call, all you need is a machine to run it on.</p>
</li>
<li>
<p><code>Submission</code> - This is a bundle of data that represents the results of grading the criteria. The data it carries is defined by you. It is sent back to the submission server (that you run). A Submission is graded "against" a set of Criteria. Any data that you need in any criterion should be in here.</p>
</li>
<li>
<p><code>helpers</code> - this is pretty self evident. It's a module (with submodules) that contains functions that easily accomplish tasks that you'll probably run into. Currently there are 3 helper modules, but more will be added:</p>
<ul>
<li><code>cli</code> - handles terminal operations like getting user input</li>
<li><code>fs</code> - file system operations, like ensuring a file exists or contains something</li>
<li><code>web</code> - make GET and POST web requests in one function call</li>
</ul>
</li>
</ul>
<p>As I said in the "Use Case" section, this was originally intended to grade TCMG labs. As a consequence, there may be some academia-oriented terminology in here. Originally, the Submission type had a mandatory student ID and name field, but I've removed those to make it more flexible. I'm trying to remove anything that would limit this application to just that use case. You may be using this for something other that what it was intended, which is fantastic.</p>
<h1><a class="header" href="#example-grader" id="example-grader">Example Grader</a></h1>
<p>This chapter will walk you through building a complete grader from start to finish. I'm writing this as I publish version <code>0.10.0</code>. There will no doubt be changes in later versions.</p>
<h2><a class="header" href="#scenario" id="scenario">Scenario</a></h2>
<p>Let's say we wrote a lab to teach the basics of Git, and we need to ensure that the sudents have done the following:</p>
<ul>
<li>Installed Git</li>
<li>Initialized Git in a repository</li>
<li>Made at least 3 commits in the repository</li>
<li>Pushed the repository to Github</li>
</ul>
<p>These are our "criteria", which is an important term. Because we have 4 criteria, let's say each is worth 25 points for a total of 100 points.</p>
<p>Let's write a grader program that the student will run. The grader will check these criteria and send a report back to us.</p>
<h2><a class="header" href="#setup" id="setup">Setup</a></h2>
<p>Our grader will be written in <a href="https://www.rust-lang.org/">Rust</a>. Before we get started, be sure you have all the necessary tools to write a Rust application, including <code>cargo</code>. You can <a href="https://www.rust-lang.org/learn/get-started">install it here</a> if you don't already have it. Be sure you're running on the "nightly" release of Rust. You can switch to nightly with <code>rustup default nightly</code>.</p>
<p>We'll make a new Rust project using <code>cargo</code></p>
<pre><code>$ cargo new my_grader
</code></pre>
<p>This will make 3 files for us. <code>Cargo.toml</code> is where we specify details about our application (called a "crate"). You can leave most of it alone, as we won't be publishing this crate, but you need to add this crate (<code>lab_grader</code>) as a dependency.</p>
<pre><code class="language-toml">[dependencies]
lab_grader = "0.10.0"
</code></pre>
<p><code>main.rs</code> contains a hello world function, so you can go ahead and compile and run your program with</p>
<pre><code>$ cargo run
</code></pre>
<p>After it compiles you should see "Hello, world!". Now we can move on to <a href="example_grader/./criteria.html">defining the criteria</a>.</p>
<h1><a class="header" href="#defining-the-criteria" id="defining-the-criteria">Defining the Criteria</a></h1>
<p>Once we have our project set up, the first step is to define our criteria. Criteria are contained within a "Rubric". A Rubric has a name, description, and a list of criteria. It's represented in Rust by the <code>Rubric</code> structure. We're going to write a <code>yaml</code> file, and all the data we put in there will be serialized into a <code>Rubric</code>.</p>
<p>Let's make a directory called <code>criteria/</code>, and inside there we'll make a file called <code>rubric.yml</code>:</p>
<pre><code class="language-yaml"># criteria/rubric.yml
name: Git Lab
desc: Install and use Git
</code></pre>
<p>Here we've put a name for our rubric, and then an (optional) description.</p>
<p>Next we'll add our criteria. You can see the <a href="example_grader/../criteria/yaml_spec.html">YAML specification</a> to see all the keys that are available here. As a reminder, here's the criteria we wrote out in the last section:</p>
<ul>
<li>Installed Git</li>
<li>Initialized Git in a repository</li>
<li>Made at least 3 commits in the repository</li>
<li>Pushed the repository to Github</li>
</ul>
<pre><code class="language-yaml"># criteria/rubric.yml
name: Git Lab
desc: Install and use Git
criteria:
"Git installed":
stub: git-installed
index: 1
worth: 25
messages: ["installed", "not installed"]
"Git initialized":
stub: git-init
index: 2
worth: 25
messages: ["initialized", "uninitialized"]
"Commits present":
stub: commits-present
index: 3
worth: 25
messages: [">= 3 commits found", "< 3 commits found"]
"Repo pushed":
stub: repo-pushed
index: 4
worth: 25
messages: ["pushed", "not pushed"]
</code></pre>
<p>We've put 4 criteria in the <code>yaml</code> file. Each one has a name, a stub (an identifier), a point value, index, and some success/failure messages. The name and worth are the only required fields.</p>
<p>Once again, see the <a href="example_grader/">YAML specification</a> for more info on what you can put in this file.</p>
<p>Now we have our criteria defined, we can move on to <a href="example_grader/submission.html">writing some Rust</a>.</p>
<h1><a class="header" href="#building-a-submission" id="building-a-submission">Building a Submission</a></h1>
<p>A submission is a bundle of data that represents a students work. A submission is graded against a rubric, and then sent back to you. By default, it contains a timestamp, a numerical grade, and 2 lists of the criteria that the student passed/failed.</p>
<p>You can add any kind of data that you might want, for example the students name and ID, or information about their system like IP address.</p>
<p>Any data that is needed from inside a criteria test should also be here. This will make more sense when we write the criteria tests.</p>
<h2><a class="header" href="#some-housekeeping" id="some-housekeeping">Some Housekeeping</a></h2>
<p>We need to do some housekeeping in our <code>main.rs</code></p>
<pre><code class="language-rust noplaypen">extern crate lab_grader;
use lab_grader::*;
fn main() {
// code will go here
}
</code></pre>
<p>We added an import to the top to bring in all the items we'll need from <code>lab_grader</code>. Then we just cleared our <code>main</code> function. In the next section, we'll add code into the <code>main</code> function.</p>
<h2><a class="header" href="#build-a-submission" id="build-a-submission">Build a Submission</a></h2>
<p>Now we can build a submission, which the <code>Submission::new</code> function. Add the following to the beginning of your <code>main</code> function.</p>
<pre><code class="language-rust noplaypen">let mut sub = Submission::new();
</code></pre>
<p>We make it mutable to we can attach data later.</p>
<h2><a class="header" href="#attach-data" id="attach-data">Attach Data</a></h2>
<p>We want some data to attach to the submission. In this case, we're going to want the student's name and ID, as well as their Github username and the name of the repository they create for the lab. We'll use this data a little later.</p>
<p>We're going to use two macros to make this data:</p>
<ul>
<li><code>data!</code> - creates a bundle of key/value pairs</li>
<li><code>prompt!</code> - asks the user for input from the terminal</li>
</ul>
<pre><code class="language-rust noplaypen">fn main() {
let mut sub = Submission::new();
// Create data
let data = data! {
"name" => prompt!("Name: ", String),
"id" => prompt!("ID: ", String),
"gh_name" => prompt!("Github Username: ", String),
"repo" => prompt!("Repo name: ", String)
};
// Attach data to submission
sub.use_data(data);
}
</code></pre>
<h2><a class="header" href="#refactor" id="refactor">Refactor</a></h2>
<p>We can refactor the code above into this, using the <code>Submission::from_data</code> function.</p>
<pre><code class="language-rust noplaypen">fn main() {
let mut sub = Submission::from_data(data! {
"name" => prompt!("Name: ", String),
"id" => prompt!("ID: ", String),
"gh_name" => prompt!("Github Username: ", String),
"repo" => prompt!("Repo name: ", String)
});
}
</code></pre>
<h2><a class="header" href="#test" id="test">Test</a></h2>
<p>Add the following line to the end of main and run the program with <code>cargo run</code> to see what it does so far.</p>
<pre><code class="language-rust noplaypen">println!("{:#?}", sub);
</code></pre>
<p>Now we can move on to <a href="example_grader/rubric.html">building the rubric</a>.</p>
<h1><a class="header" href="#building-the-rubric" id="building-the-rubric">Building the Rubric</a></h1>
<p>In <a href="example_grader/criteria.html">the first section</a> we defined our criteria in a <code>yaml</code> file. Now we need to load the <code>yaml</code> data into Rust and build a <code>Rubric</code> from it.</p>
<h2><a class="header" href="#a-note-about-errors" id="a-note-about-errors">A Note about Errors</a></h2>
<p>In Rust, the main way that errors are handled in through the <a href="https://doc.rust-lang.org/std/result/"><code>Result</code></a> and <a href="https://doc.rust-lang.org/std/option/"><code>Option</code></a> types. These are massively important to Rust, and you should read over them and learn how they work.</p>
<p>A very good guide is from the <a href="https://doc.rust-lang.org/stable/rust-by-example/error.html">Rust by Example</a> book. The "Error Handling" section should take less than an hour to read over and will be very useful if you continue with Rust.</p>
<p>I'm going to be using the <code>expect</code> method, which is normally bad practice. It simply panics (aborts) with an error message if there's an error. Normally you would want to deal with the error in one way or another, but I'm going to use this anyway since it's just an example. <code>expect</code> should <em>not</em> be used in production code.</p>
<h2><a class="header" href="#reading-yaml-data" id="reading-yaml-data">Reading YAML Data</a></h2>
<p>We can read YAML data with the <code>yaml!</code> macro. <code>yaml!</code> takes in a relative file path and returns the YAML data as a <code>String</code>.</p>
<p>This macro is very important for one reason. When you compile in debug mode (default), this macro will read from the file system as expected. However, when you compile for release (with the <code>--release</code> flag), it will read the file contents and embed the contents in the created executable. This means when you distribute the grader to your students, you don't need to provide the <code>yaml</code> file. The executable will run on it's own. Just be sure to compile in release mode before distributing.</p>
<p>We can go ahead and add this to the end of our <code>main</code> function</p>
<pre><code class="language-rust noplaypen">let yaml = yaml!("../criteria/rubric.yml").expect("Couldn't read file");
</code></pre>
<h2><a class="header" href="#building-a-rubric" id="building-a-rubric">Building a Rubric</a></h2>
<p>Now that we have our yaml data, we can build a <code>Rubric</code> from it.</p>
<pre><code class="language-rust noplaypen">let mut rubric = Rubric::from_yaml(yaml).expect("Bad yaml!");
</code></pre>
<p>Here we're using the <code>expect</code> method again, but it's probably a good idea in this case. This will crash if we have invalid YAML or missing items. Once you're done developing and compile for release, the YAML will be embedded and won't change, so it won't crash after that.</p>
<p>That's all there is to building a rubric. Here's the complete <code>main.rs</code> file so far</p>
<pre><code class="language-rust noplaypen">extern crate lab_grader;
use lab_grader::*;
fn main() {
let mut sub = Submission::from_data(data! {
"name" => prompt!("Name: ", String),
"id" => prompt!("ID: ", String),
"gh_name" => prompt!("Github Username: ", String),
"repo" => prompt!("Repo name: ", String)
});
let yaml = yaml!("../criteria/rubric.yml").expect("Couldn't read file");
let mut rubric = Rubric::from_yaml(yaml).expect("Bad yaml!");
}
</code></pre>
<p>Now we can move on to <a href="example_grader/tests.html">writing the criteria tests</a>.</p>
<h1><a class="header" href="#writing-the-tests" id="writing-the-tests">Writing the Tests</a></h1>
<p>Now comes the most important part of writing the application. We've build our rubric of criteria, but they currently don't have a way to be tested. How do we know if they actually have Git installed or not?</p>
<p>The way we determine this is by writing a "test", which is just a function, for each of our criteria. Then we'll "attach" the function to the criteria, then we can grade the submission.</p>
<h2><a class="header" href="#a-single-test" id="a-single-test">A Single Test</a></h2>
<p>Each test needs to have the same signature, meaning it has to accept the same parameters and return the same thing. We need this consistency between tests to make grading possible.</p>
<p>Every test must accept a reference to a <code>TestData</code> object, and return a boolean. We created a <code>TestData</code> object with the <code>data!</code> macro when we made a submission. In fact, the exact data we put in the submission will be the data passed into each of our criteria tests. This is why we put the users Github username and repository name in the data; we'll need it inside one of our tests.</p>
<h2><a class="header" href="#helpers" id="helpers">Helpers</a></h2>
<p>Before we write any tests, you should know about the <a href="example_grader/../helpers/home.html">helper</a> modules. These modules are a collection of functions that do common tasks in criteria tests. They may save you some times. See the documentation linked above for more info on each module.</p>
<h2><a class="header" href="#the-first-test" id="the-first-test">The First Test</a></h2>
<p>Let's write a test for our first criteria, which checks if Git is installed or not. Remember that a test is just a function with a specific signature.</p>
<p>You can write your tests anywhere, but I'll make a <code>tests.rs</code> to keep the tests separate from the rest of our code.</p>
<pre><code class="language-rust noplaypen">// tests.rs
use lab_grader::*;
fn confirm_git_installed(_: &TestData) -> bool {
cli::Program::Git.version().is_some()
}
</code></pre>
<p>We added a function called <code>confirm_git_installed</code>. It takes in a parameter of type <code>&TestData</code>, but in this case we don't need it so we'll name the parameter <code>_</code>. In the function body, we used the <code>cli</code> helper module to get the version of Git, and returned true if it's a <code>Some</code> value (this is an <code>Option</code> type, it would be <code>None</code> if Git wasn't installed).</p>
<p>And that's it for the first test. There's still one step to go, but we'll do that after the other tests.</p>
<h2><a class="header" href="#the-rest-of-the-tests" id="the-rest-of-the-tests">The Rest of the Tests</a></h2>
<p>I'm going to write some functions to serve as the tests for the remaining criteria. I won't explain what each one is doing in detail, but it should be pretty self explanatory. I'll put the entire <code>tests.rs</code> file here.</p>
<pre><code class="language-rust noplaypen">use std::process::Command;
use lab_grader::*;
// Naming the data parameter "_" because we don't need it in this case
pub fn confirm_git_installed(_: &TestData) -> bool {
cli::Program::Git.version().is_some()
}
pub fn confirm_git_init(_: &TestData) -> bool {
// This is a filesystem helper that this crate provides
// also works on directories
// This is *not* std::fs
fs::file_exists(".git/")
}
pub fn confirm_enough_commits(_: &TestData) -> bool {
// Run the git command to list commit count
let out = Command::new("sh")
.arg("-c")
.arg("git rev-list --all --count")
.output()
.expect("Couldn't run subcommand");
// If the command returns something
if let Ok(string) = String::from_utf8(out.stdout) {
// And if we could parse a number from it
if let Ok(num) = string.trim().parse::<u64>() {
return num > 2;
}
}
false
}
// We do need the data this time
pub fn confirm_repo_pushed(data: &TestData) -> bool {
// Format the url to check
let url = format!("https://github.com/{}/{}/", data["gh_name"], data["repo"]);
// Another helper function
web::site_responds(&url)
}
</code></pre>
<p>In the <code>confirm_repo_pushed</code>, we're actually using the data attached to the submission. We can do that with bracket syntax (<code>data["key"]</code>) or through the <a href="https://doc.rust-lang.org/beta/std/collections/struct.HashMap.html#method.get"><code>get</code></a> method. Using <code>get</code> is recommended over bracket syntax.</p>
<h2><a class="header" href="#attaching-the-tests" id="attaching-the-tests">Attaching the Tests</a></h2>
<p>We need to import the tests we wrote in <code>main.rs</code>. Add this import to the top of <code>main.rs</code></p>
<pre><code class="language-rust noplaypen">// Declare the tests mod (tests.rs)
mod tests;
// Import all the test functions
use tests::*;
</code></pre>
<p>Now that we have our tests, we need to attach them to the appropriate criteria. We can do that with the <code>attach!</code> macro.</p>
<pre><code class="language-rust noplaypen">fn main() {
// ...
attach! {
rubric,
"git-installed" => confirm_git_installed,
"git-init" => confirm_git_init,
"commits-present" => confirm_enough_commits,
"repo-pushed" => confirm_repo_pushed
};
}
</code></pre>
<p>This attached each of our functions to the criteria with the given stub. This is why we needed to specify a stub in yaml.</p>
<blockquote>
<p>Note: If you don't provide the stub in yaml, it will be created by lowercasing the name and replacing whitespace with a dash. ie. <code>My First Criterion => my-first-criterion</code>.</p>
</blockquote>
<p>Now that the tests are attached, we're <a href="example_grader/grade.html">ready to grade</a>.</p>
<h1><a class="header" href="#grading" id="grading">Grading</a></h1>
<p>We now have a complete <code>Rubric</code> and a <code>Submission</code>, which is all we need to grade.</p>
<p>When we grade the submission, we'll run each criteria test in the rubric. The submission's data will be passed into each of the tests. If the test passes, the submissions <code>grade</code> field will increase by the worth of the criterion.</p>
<pre><code class="language-rust noplaypen">fn main() {
// ...
// Grade the submission
sub.grade_against(&mut rubric);
// Print the rubric results
println("{}", rubric);
}
</code></pre>
<p>We've graded the submission and then printed the rubric. Printing the rubric will show the student all the criteria and let them know what they passed or failed. Of course, you don't have to do this. You may want to keep one or all of your criteria private. You can hide individual criteria with the <code>hide</code> field in yaml, and you can always just not print the rubric.</p>
<p>That's all there is to grading. The next step is to submit to a submission server, which we'll do in the <a href="example_grader/submit.html">next section</a>.</p>
<h1><a class="header" href="#submitting" id="submitting">Submitting</a></h1>
<p>When a student runs the program, it should grade their submission and then submit to a location you, as the professor/TA, can access. There's two parts to this.</p>
<h2><a class="header" href="#submission-server" id="submission-server">Submission Server</a></h2>
<p>The submission server is simply a web server with a single route: it accepts POST requests on the <code>/submit</code> route. You can run it with a single function.</p>
<p>You'll want to set up a publicly accessible server to run this server. I use a Microsoft Azure VM because they're pretty easy to set up and provide DNS services.</p>
<p>Let's add a little bit of code to the <em>beginning</em> of our <code>main</code> function. It will read the command line arguments and run the server if you run the program with the "<code>server</code>" argument.</p>
<pre><code class="language-rust noplaypen">fn main() {
// Get command line arguments
let args: Vec<String> = std::env::args().collect();
// If the second one is "server"
if args.len() == 2 && args[1] == "server" {
// Run the server on port 8080
Submission::server(8080);
}
// ...
}
</code></pre>
<p>Now you can run the program with the "<code>server</code>" argument to start up your web server. Open another terminal and run this server in the background while we finish the grader.</p>
<h2><a class="header" href="#submitting-1" id="submitting-1">Submitting</a></h2>
<p>Now that the submission server is running, we can submit. Let's add this to the end of the <code>main</code> function.</p>
<pre><code class="language-rust noplaypen">fn main() {
// ...
let url = "http://localhost:8080/submit";
let res = web::post_json(url, &sub);
if let Ok(response) = res {
println!("Submission sent, response {}", response.status());
} else {
println!("Error sending submission! {}", res.unwrap_err());
}
}
</code></pre>
<p>This will submit the submission to the server that you should have running, and print a success or error message. Of course, you'll want to put the url for your server instead of <code>localhost</code>.</p>
<h1><a class="header" href="#the-results" id="the-results">The Results</a></h1>
<p>Once a submission is accepted by the submission server, it will create a file called <code>submissions.csv</code>. Every submission accepted will be written to this file. This is a simple csv file, and you can open it in Excel or another program to process the data in any way you see fit.</p>
<p>A few warnings:</p>
<ul>
<li>The header of the csv file will be written for <em>the first</em> submission recieved. If, for some reason, submissions have different data fields, the csv values and header won't match up. You should be sure that submissions all have the same data fields.</li>
<li>You should try to avoid having commas in criteria names/descriptions, or in your data. You can't really prevent users from entering commas though. When submitting, any commas found will be replaced with semicolons.</li>
<li>You need to compile the grader on the platform that your students will be running it on. If you want to provide a version for Windows, Linux, and MacOS, I recommend using a <a href="https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow">Github workflow</a> to build for each platform.</li>
</ul>
<h2><a class="header" href="#wrapping-up" id="wrapping-up">Wrapping up</a></h2>
<p>This has been a very simple example of a very simple grader. The last thing to do is distribute the program to your students. when you compile for release mode, it will generate an executable in <code>target/release/[program_name]</code></p>
<pre><code>$ cargo build --release
</code></pre>
<p>We usually write our labs as repositories on Github that the student can clone and follow, so we put the grader in the repo.</p>
<h1><a class="header" href="#criteria" id="criteria">Criteria</a></h1>
<p>coming soon...</p>
<h1><a class="header" href="#yaml-specification" id="yaml-specification">YAML Specification</a></h1>
<p>Here's the keys allowed when making a yaml file for a rubric. Not all fields are required, but it's better to be specific.</p>
<p>Here's the <a href="https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html">YAML syntax</a> so you can learn how to write valid YAML. This isn't the official specification, but it's the easiest guide I found.</p>
<p>When naming your yaml files, you can use <code>.yml</code> or <code>.yaml</code>. Honestly you can use whatever extension, I don't care, but I use <code>.yml</code> for mine.</p>
<p>Quotes around strings are usually not required.</p>
<hr />
<h2><a class="header" href="#minimum-required" id="minimum-required">Minimum Required</a></h2>
<pre><code class="language-yaml">name: Minimum Rubric
criteria:
Only criterion:
worth: 10
</code></pre>
<h2><a class="header" href="#everything" id="everything">Everything</a></h2>
<pre><code class="language-yaml"># Required
name: Rubric Name
# Optional
desc: A short description about your rubric
# Optional
# This is just an extra check to make sure
# all criteria add to the correct total
# If they don't, it will print an error message when running.
total: 25
# Required
# You need at least one criteria
criteria:
# Required
# Quotes are not required
Criterion name:
# Optional but recommended
# This will be inferred from the name (lowercased, whitespace => '-')
# This is like a human readable ID.
# Must be unique.
# This can really be any string, but it's best to keep it short and whitespace-free
stub: a-unique-stub
# Required
# Point value of the criterion
# This is completely subjective, you give it worth
# Can be negative
worth: 15
# Optional
# Controls the order the criteria are run
# Lowest first
# Can be negative
index: 5
# Optional
desc: More information about this criterion
# Optional
# Printed to the console when a criterion passes or fails
# Defaults to these values
messages: ["passed", "failed"]
# Optional
# if this is true, criterion cannot be printed
# Defaults to false
hide: false
# Here's all the fields without the comments
Second criterion:
stub: second-criterion
worth: 10
index: 1
desc: Here's some more about this criterion
messages: ["passed", "failed"]
hide: false
</code></pre>
<h1><a class="header" href="#a-single-criterion" id="a-single-criterion">A Single Criterion</a></h1>
<p>A <code>Criterion</code> is the bread and butter of this application. To help us understand it a bit better, I'm going to put the definition of the <code>Criterion</code> struct here so we can take it apart.</p>
<p>Detailed <code>Criterion</code> documentation available <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/criterion/struct.Criterion.html">here on docs.rs</a>.</p>
<pre><code class="language-rust noplaypen">// Comments elided
pub struct Criterion {
pub stub: String,
pub name: String,
pub worth: i16,
pub index: i64,
pub messages: (String, String),
pub desc: Option<String>,
pub test: Box<dyn Fn(&TestData) -> bool>,
pub status: Option<bool>,
pub hide: bool,
}
</code></pre>
<p>If you look through these fields, you might recognize them as the fields allowed in the <a href="criteria/./yaml_spec.html">YAML specification</a>. In fact, the only field that isn't present in the YAML spec is the <code>test</code> field. This is because <code>test</code> is a function, and we can't write Rust functions from inside YAML.</p>
<p>You should pretty much always create criteria from YAML, and parse it into a <code>Rubric</code> type. A single criteria alone isn't really useful. Nonetheless, there is a <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/criterion_builder/struct.CriterionBuilder.html"><code>CriterionBuilder</code></a> struct that helps you create them individually. If you want you can create a list of criteria that way then throw them into a rubric. But this is much more work than through YAML.</p>
<p>The best reason I can think of for creating a <code>Criterion</code> manually is if you want to programmatically change a piece of data like the name or worth. But, you can always define it in YAML and change just that field later.</p>
<p>Really all a <code>Criterion</code> can do is run it's own test with or without data, and return a result. Again, they aren't very useful alone.</p>
<h1><a class="header" href="#a-criteria-test" id="a-criteria-test">A Criteria Test</a></h1>
<h1><a class="header" href="#a-rubric" id="a-rubric">A Rubric</a></h1>
<p>A <code>Rubric</code> is a collection of criteria. It is represented in Rust as the <a href="https://docs.rs/lab_grader/0.11.0/lab_grader/rubric/struct.Rubric.html"><code>Rubric</code></a> struct.</p>
<p>A rubric is the primary way that you, as the programmer, interact with a collection of criteria. Rubrices are created by parsing YAML data into criteria, then attaching tests to those criteria.</p>
<p>Detailed <code>Rubric</code> documentation <a href="https://docs.rs/lab_grader/0.11.0/lab_grader/rubric/struct.Rubric.html">here on docs.rs</a>.</p>
<h2><a class="header" href="#creating-a-rubric" id="creating-a-rubric">Creating a Rubric</a></h2>
<p>See the <a href="criteria/./yaml_spec.html">YAML specification</a> for more information on what is allowed and required in YAML. Here's a short example</p>
<pre><code class="language-yaml">name: My First Rubric
desc: An optional description about what this rubric does
criteria:
"First criterion"
stub: first-crit
worth: 25
"Second criterion"
stub: second-crit
worth: 25
</code></pre>
<p>Now we can deserialize that YAML into a <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/rubric/struct.Rubric.html"><code>Rubric</code></a> struct.</p>
<pre><code class="language-rust noplaypen">let yaml = yaml!("path/to/that/yaml.yml").unwrap();
let mut rubric = Rubric::from_yaml(yaml).expect("Bad yaml!");
</code></pre>
<h2><a class="header" href="#using-a-rubric" id="using-a-rubric">Using a Rubric</a></h2>
<p>Rubrices can do a few things on their own, but they're meant to be used with a <code>Submission</code>. This is what a Submission is graded against.</p>
<p>Here's a few code examples of what you can do with a <code>Rubric</code> alone.</p>
<pre><code class="language-rust noplaypen">// These print the rubric and all the criteria contained in it
println!("{}", rubric); // Print a rubric in full
rubric.print_short(); // Print a shorter version
rubric.print_table(); // print a full table
// This gets the total points possible
println!("Total points possible: {}", rubric.total_points());
// This gets the total points earned. You should grade before running this
println!("Points earned: {}", rubric.points());
// Adds a criterion to the list
rubric.add(/* some criterion */);
// Gets a criterion by stub, if any
if let Some(crit) = rubric.get("first-crit") {
println!("criterion name: {}", crit.name);
}
// Attaches a function to a particular criteria
fn my_func(_: &TestData) -> bool { true };
if let Some(crit) = rubric.get("first-crit") {
crit.attach(Box::new(my_func));
}
</code></pre>
<h2><a class="header" href="#the-attach-macro" id="the-attach-macro">The <code>attach!</code> macro</a></h2>
<p>The <code>attach!</code> macro is provided to easily attach many tests to criteria. In the code example above, you can see how to define a function and then attach it to a criterion through the <code>Rubric::attach</code> function. The <code>attach!</code> macro allows you to do many of these at once, and you don't have to <code>Box</code> it either.</p>
<pre><pre class="playpen"><code class="language-rust noplayplen">fn test_func1(_: &TestData) -> bool { true };
fn test_func2(_: &TestData) -> bool { false };
fn main() {
// create the rubric from yaml as above...
attach! {
rubric,
"first-crit" => test_func1,
"second-crit" => test_func2
};
}
</code></pre></pre>
<p>This is the recommended way to attach functions to criteria.</p>
<h1><a class="header" href="#submission" id="submission">Submission</a></h1>
<p>A <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/submission/struct.Submission.html"><code>Submission</code></a> represents one students work on a lab. It's a bundle of data that can have whatever you want in it. A <code>Submission</code> works together with a <code>Rubric</code>. A <code>Submission</code> is graded against a <code>Rubric</code> and assigned a grade.</p>
<p>After a <code>Submission</code> is graded, it can be sent back to you, as the professor/TA, for review.</p>
<p>Submissions come with a timestamp by default. This will be the timezone of the system it was submitted from, in <a href="https://tools.ietf.org/html/rfc3339">rfc3339</a> format. It also has a numerical grade that will be changed when grading it. The <code>passed</code> and <code>failed</code> fields store which criteria this submission passed or failed.</p>
<p>The most important part of a submission is it's data, which you can read about in <a href="submission/./test_data.html">the next section</a>.</p>
<h1><a class="header" href="#testdata" id="testdata">TestData</a></h1>
<p>The <code>TestData</code> type is simply a type alias to the <a href="https://doc.rust-lang.org/beta/std/collections/struct.HashMap.html"><code>HashMap</code></a> type. Any methods or functionality that <code>HashMap</code> provides is also on <code>TestData</code>.</p>
<p>This field is part of a <code>Submission</code> and is meant to hold 2 things:</p>
<ol>
<li>Data you need in the final submission, and</li>
<li>Data you need from within a <a href="submission/../criteria/test.html">criteria test</a>.</li>
</ol>
<p>The only restriction is that the <code>TestData</code> type is equivilent to a <code>HashMap<String, String></code>, meaning both the keys and values must be a <code>String</code> type.</p>
<h2><a class="header" href="#creating" id="creating">Creating</a></h2>
<p>The best way to create a <code>TestData</code> bundle is through the <code>data!</code> macro.</p>
<pre><code class="language-rust noplaypen">let data = data! {
"key" => "value",
"key2" => "value 2"
};
assert_eq!( data["key"], "value" );
</code></pre>
<p>The <code>data!</code> and the <code>prompt!</code> macro work very well together. You can read about the <code>prompt!</code> macro in <a href="submission/./prompt.html">the next section</a>.</p>
<h1><a class="header" href="#the-prompt-macro" id="the-prompt-macro">The <code>prompt!</code> macro</a></h1>
<p>You're probably going to want to get some user input and put that into the submission's data. For instance, a submission isn't very useful if you don't know who sent it. You'll want a name and possibly ID.</p>
<p>The <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/macro.prompt.html"><code>prompt!</code></a> macro will ask for user input and try to cast it to the given type.</p>
<pre><code class="language-rust noplaypen">let name = prompt!("Name: ", String);
println!("Your name is {}", name);
</code></pre>
<p>This macro can also enfore that the user enters a certain type. If it can't cast what they entered into the given type, it will crash with an error message.</p>
<pre><code class="language-rust noplaypen">let number = prompt!("Enter a number: ", isize);
println!("{} is definitely a number.", number);
</code></pre>
<blockquote>
<p>Note: the values of a <code>TestData</code> object must be a string, so you'll need to cast whatever they entered <em>back to a <code>String</code></em> if you want to include it in the <code>TestData</code> object.</p>
</blockquote>
<h1><a class="header" href="#creating-a-submission" id="creating-a-submission">Creating a Submission</a></h1>
<p>We can combine what we know about about submissions, <a href="submission/./test_data.html"><code>TestData</code></a>, and <a href="submission/./test_data.html#the-prompt-macro"><code>prompt!</code></a> to make a complete submission.</p>
<p>This snippet will make a new submission, ask the user for their name and ID, then wrap those into a <code>TestData</code> object and attach it to the submission.</p>
<pre><code class="language-rust noplaypen">let sub = Submission::from_data(data! {
"name" => prompt!("Name: ", String),
"id" => prompt!("ID: ", String)
});
</code></pre>
<h1><a class="header" href="#submitting-2" id="submitting-2">Submitting</a></h1>
<p>You can send a submission with a POST request. The easiest way to do this is through the <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/helpers/web/fn.post_json.html"><code>post_json</code></a> method from the <code>helpers::web</code> module.</p>
<p>The <code>Submission</code> type is JSON serializable, so we don't have to do any extra work before sending.</p>
<pre><code class="language-rust noplaypen">let sub = Submission::from_data(/* data here */);
// application code here...
let url = "http://localhost:8080/submit";
web::post_json(url, &sub);
</code></pre>
<p>Here, I'm POSTing the submission to a server running on <code>localhost:8080</code>. There's a special web server meant to handle these requests, which you can read about in the <a href="submission/./server.html">submission server</a> section.</p>
<h2><a class="header" href="#error-handling" id="error-handling">Error Handling</a></h2>
<p>The <code>web::post_json</code> method returns a <code>Result</code> with a <a href="https://docs.rs/reqwest/0.10.4/reqwest/blocking/struct.Response.html"><code>Response</code></a> type inside, which can be used to handle errors. There's 101 ways you could do this, but here's an example.</p>
<pre><code class="language-rust noplaypen">let url = "http://localhost:8080/submit";
if let Ok(resp) = web::post_json(url, &sub) {
if resp.status().is_success() {
// if the response has a success code
println!("Success! server responded with {}", resp.status());
} else {
// otherwise it came back with an error
println!("Error! the server responded with {}", resp.status());
}
} else {
// This means the response couldn't even be completed
println!("Error! The request could not be performed!");
}
</code></pre>
<h1><a class="header" href="#submission-server-1" id="submission-server-1">Submission Server</a></h1>
<p>The "Submission Server" is the other half to <a href="submission/./submit.html">submitting</a>. It's a preconfigured web server built to handle submissions.</p>
<p>You can run it through the <code>Submission</code> type like this</p>
<pre><code class="language-rust noplaypen">// Running on port 8080
Submission::server(8080);
</code></pre>
<p>This will run a web server with two routes:</p>
<ol>
<li><code>GET /</code> - returns an <code>OK</code> status with no other content</li>
<li><code>POST /submit</code> - Accepts a JSON body of a <code>Submission</code>, returns an <code>Accepted</code> code on success, and an <code>InternalServerError</code> on failure.</li>
</ol>
<h2><a class="header" href="#running-the-server" id="running-the-server">Running the server</a></h2>
<p>I like to set up a Microsoft Azure server to run this web server on, but you can do whatever you like. It just needs to be publicly accessible with a static IP or DNS name. You should put the address to your server (including the <code>/submit</code> bit) as the url to POST the submission to.</p>
<p>Be sure your server has the proper ports exposed. If you want to run on port 80 and omit the port in the url, be sure to run with root permission.</p>
<p>I like to run the server from within my grader through a command line parameter. Here's an example.</p>
<pre><code class="language-rust noplaypen">use std::env;
// ...
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() == 2 && args[1] == "server" {
Submission::server(8080);
}
}
</code></pre>
<h2><a class="header" href="#the-results-1" id="the-results-1">The Results</a></h2>
<p>When the server accepts the first submission, it will make a file (in the directory you run the server in) called <code>submissions.csv</code>. It will then write the submission (including all the data you put in it) to the csv file.</p>
<p>It's important to note that the header for the csv file will be written according to the <em>first</em> submission it accepts. If, for some reason, different submissions has different data keys, the csv file will break. This is because one submission should have more columns and values than another. You should be sure not to conditionally add data to a submission.</p>
<h1><a class="header" href="#helpers-1" id="helpers-1">Helpers</a></h1>
<p>This chapter contains examples of all the helper functions. If you need more specific documentation, look on <a href="https://docs.rs/lab_grader/0.10.0/lab_grader/helpers/index.html">docs.rs</a>.</p>
<h2><a class="header" href="#importing" id="importing">Importing</a></h2>
<p>You can import all the helper modules like this</p>
<pre><code class="language-rust noplaypen">use lab_grader::helpers::*;
</code></pre>
<p>Or import any specific helper module with</p>
<pre><code class="language-rust noplaypen">use lab_grader::[module_name];
</code></pre>
<h1><a class="header" href="#file-system---helpersfs" id="file-system---helpersfs">File System - <code>helpers::fs</code></a></h1>
<p>File system operations. Don't get this confused with <code>std::fs</code>. It's not the same.</p>
<h2><a class="header" href="#file_exists" id="file_exists"><code>file_exists</code></a></h2>
<p>Returns true if the file (or directory!) exists. Relative to the file that the compiled executable is being run in. If you're paranoid, use <a href="https://doc.rust-lang.org/std/fs/fn.canonicalize.html"><code>std::fs::canonicalize</code></a> to get a full path.</p>
<pre><code class="language-rust noplaypen">assert!( fs::file_exists("Cargo.toml") );
</code></pre>
<h2><a class="header" href="#file_contains" id="file_contains"><code>file_contains</code></a></h2>
<p>Returns true if a file contains the provided string. Case sensitive.</p>
<pre><code class="language-rust noplaypen">assert!( fs::file_contains("Cargo.toml", "version") );
</code></pre>
<h1><a class="header" href="#web---helpersweb" id="web---helpersweb">Web - <code>helpers::web</code></a></h1>
<p>These functions relate to making web requests. They use the <a href="https://docs.rs/reqwest/0.10.4/reqwest/"><code>reqwest</code></a> crate.</p>
<p>All of these functions use the <code>blocking</code> feature of reqwest, meaning they'll block until the request returns. You can make asynchronous requests with the reqwests crate, but that would involve more complicated code. I'm trying to keep it simple.</p>
<h2><a class="header" href="#get" id="get"><code>get</code></a></h2>
<p>Makes a GET request to the given URL, returning a <a href="https://docs.rs/reqwest/0.10.4/reqwest/blocking/struct.Response.html"><code>Result<Response></code></a>.</p>
<p>URL has to be valid or this will fail immediately.</p>
<p>Be cautious when making web requests; they take quite a bit of time. Only use them when necessary.</p>
<pre><code class="language-rust noplaypen">let url = "https://postman-echo.com/get";
let result = web::get(url);
assert!( result.is_ok() );
if let Ok(response) = result {
// Get the body from the response
if let Ok(body) = response.text() {
assert!( body.contains("postman-echo.com") );
}
}
</code></pre>
<h2><a class="header" href="#site_responds" id="site_responds"><code>site_responds</code></a></h2>
<p>Returns true if a site responds with a success message. This will return false on 404 codes and other error codes.</p>
<pre><code class="language-rust noplaypen">let url = "https://postman-echo.com/get";
assert!( web::site_responds(url) );
</code></pre>
<h2><a class="header" href="#post_json" id="post_json"><code>post_json</code></a></h2>
<p>This posts data in JSON format to the given URL. The parameter you pass in must implement the <a href="https://serde.rs/derive.html"><code>Serialize</code></a> trait from <a href="https://serde.rs/"><code>serde</code></a>.</p>
<pre><code class="language-rust noplaypen">let url = "https://postman-echo.com/post";
// Submission is serializable
let sub = Submission::new();
let result = web::post_json(url, &sub);
assert!( result.is_ok() );
</code></pre>
<h2><a class="header" href="#post" id="post"><code>post</code></a></h2>
<p>This is just like <code>post_json</code>, except it posts arbitrary string data. It sets the <code>CONTENT_TYPE</code> header to be <code>text/plain</code>.</p>
<pre><code class="language-rust noplaypen">let url = "https://postman-echo.com/post";
let data = "here's some data to post, wonder where it will go";
let result = web::post(url, data);
assert!( result.is_ok() );
</code></pre>
<h2><a class="header" href="#get_ip" id="get_ip"><code>get_ip</code></a></h2>
<p>This retrieves the public IP of the machine. This makes a web request, so it may take more time than you would expect. Returns <code>None</code> if it couldn't be retrieved or doesn't exist.</p>
<pre><code class="language-rust noplaypen">let IpAddr = web::get_ip();
assert!( IpAddr.is_some() );
if let Some(ip) = IpAddr {
println!("My ip is: {}", ip);
}
</code></pre>
<h1><a class="header" href="#command-lineprograms---helperscli" id="command-lineprograms---helperscli">Command Line/Programs - <code>helpers::cli</code></a></h1>
<p>Functions and macros for command line interaction</p>
<h2><a class="header" href="#prompt-macro" id="prompt-macro"><code>prompt!</code> (macro)</a></h2>
<p>This macro will prompt the user for input and try to cast it to the given type.</p>
<p>Whitespace around string ends will be trimmed.</p>
<blockquote>
<p>Warning! if the user enters something that can't be parsed into the type you specify, this will print an error message and <em>exit the current process</em></p>
</blockquote>
<pre><code class="language-rust noplaypen">use std::net::Ipv4Addr;
let string: String = prompt!("Enter a string: ", String);
let number: i64 = prompt!("Enter a number: ", i64);
let ip: Ipv4Addr = prompt!("Enter an IPv4 address: ", Ipv4Addr);
</code></pre>
<h2><a class="header" href="#prompt-function" id="prompt-function"><code>prompt</code> (function)</a></h2>
<p>This is similar to the macro <code>prompt!</code>, but if the user enter invalid input it will print an error message and run again. This ensures you always get <em>something</em> from the user. However, it won't automatically cast to a type. It just returns a <code>String</code>, which you should cast manually.</p>
<p>Whitespace around the ends will be trimmed.</p>
<pre><code class="language-rust noplaypen">let input: String = prompt("Enter something: ");
</code></pre>
<h2><a class="header" href="#program" id="program"><code>Program</code></a></h2>
<p>Currently, this enum allows you to get the installed version of a program. The current programs supported are:</p>
<ul>
<li>Git</li>
<li>Docker</li>
<li>Docker-Compose</li>
<li>Python</li>
<li>Ruby</li>
</ul>
<pre><code class="language-rust noplaypen">let version: Version = cli::Program::Git.version();
// If the program isn't installed, this will be None
assert!( version.is_some() );
// You can get the major, minor, and patch numbers of the version
assert!( version.unwrap().major() >= 1 );
</code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</nav>
</div>
<script type="text/javascript">
window.playpen_copyable = true;
</script>
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
<script type="text/javascript">
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</body>
</html>