Compare commits

...

12 Commits

Author SHA1 Message Date
Lucien Greathouse
69d1accf3f 0.5.0-alpha.3 2019-02-01 17:19:00 -08:00
Lucien Greathouse
785bdb8ecb Implement new project file name, default.project.json (#120)
* Implement new project file name, default.project.json

* Rename all test projects to default.project.json

* Update CHANGELOG

* Fix warning message typo
2019-02-01 17:06:03 -08:00
Lucien Greathouse
78a1947cec Update CHANGELOG 2019-02-01 13:07:15 -08:00
Paul Doyle
0ff59ecb4e Fix issue w/ existing files not being updated in imfs (#119)
* Fix issue w/ existing files not being updated in imfs

* Add a test for updating files
2019-01-31 20:24:42 -08:00
Lucien Greathouse
b58fed16b4 Fix uses using failure::Error 2019-01-30 10:29:38 -08:00
Lucien Greathouse
6719be02c3 Fall back to showing GraphViz source when GraphViz is not installed 2019-01-29 18:10:14 -08:00
Lucien Greathouse
8757834e07 Improve error reporting for IO issues 2019-01-29 17:29:47 -08:00
Lucien Greathouse
aa243d1b8a Add sweet new live sync homepage 2019-01-28 18:30:42 -08:00
Lucien Greathouse
aeb18eb124 Refactor web code to make routing more clear 2019-01-28 18:23:57 -08:00
Lucien Greathouse
6c3e118ee3 Sort inputs in LiveSession 2019-01-28 17:50:47 -08:00
Lucien Greathouse
3c0fe4d684 Reduce number of threads needed for FsWatcher 2019-01-28 17:11:01 -08:00
Lucien Greathouse
12fd9aa1ef Tack on Cargo.lock, missing from previous commit 2019-01-28 16:03:12 -08:00
24 changed files with 462 additions and 223 deletions

View File

@@ -2,6 +2,16 @@
## [Unreleased] ## [Unreleased]
## [0.5.0 Alpha 3](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.3) (February 1, 2019)
* Changed default project file name from `roblox-project.json` to `default.project.json` ([#120](https://github.com/LPGhatguy/rojo/pull/120))
* The old file name will still be supported until 0.5.0 is fully released.
* Added warning when loading project files that don't end in `.project.json`
* This new extension enables Rojo to distinguish project files from random JSON files, which is necessary to support nested projects.
* Added new (empty) diagnostic page served from the server
* Added better error messages for when a file is missing that's referenced by a Rojo project
* Added support for visualization endpoints returning GraphViz source when Dot is not available
* Fixed an in-memory filesystem regression introduced recently ([#119](https://github.com/LPGhatguy/rojo/pull/119))
## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019) ## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
* Added support for `.model.json` files, compatible with 0.4.x * Added support for `.model.json` files, compatible with 0.4.x
* Fixed in-memory filesystem not handling out-of-order filesystem change events * Fixed in-memory filesystem not handling out-of-order filesystem change events

8
Cargo.lock generated
View File

@@ -345,7 +345,7 @@ dependencies = [
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.5.13" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1248,11 +1248,11 @@ dependencies = [
[[package]] [[package]]
name = "rojo" name = "rojo"
version = "0.5.0-alpha.1" version = "0.5.0-alpha.3"
dependencies = [ dependencies = [
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1929,7 +1929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86" "checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" "checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7" "checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
"checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" "checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
"checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" "checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"

View File

@@ -1,6 +1,6 @@
return { return {
codename = "Epiphany", codename = "Epiphany",
version = {0, 5, 0, "-alpha.2"}, version = {0, 5, 0, "-alpha.3"},
expectedServerVersionString = "0.5.0 or newer", expectedServerVersionString = "0.5.0 or newer",
protocolVersion = 2, protocolVersion = 2,
defaultHost = "localhost", defaultHost = "localhost",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.5.0-alpha.2" version = "0.5.0-alpha.3"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "A tool to create robust Roblox projects" description = "A tool to create robust Roblox projects"
license = "MIT" license = "MIT"
@@ -22,7 +22,7 @@ bundle-plugin = []
[dependencies] [dependencies]
clap = "2.27" clap = "2.27"
csv = "1.0" csv = "1.0"
env_logger = "0.5" env_logger = "0.6"
failure = "0.1.3" failure = "0.1.3"
log = "0.4" log = "0.4"
maplit = "1.0.1" maplit = "1.0.1"

54
server/assets/index.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Rojo</title>
<style>
* {
margin: 0;
padding: 0;
font: inherit;
}
html {
font-family: sans-serif;
height: 100%;
}
body {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.main {
padding: 1rem;
text-align: center;
margin: 0 auto;
width: 100%;
max-width: 60rem;
background-color: #efefef;
border: 1px solid #666;
border-radius: 4px;
}
.title {
font-size: 2rem;
font-weight: bold;
}
.docs {
font-size: 1.5rem;
font-weight: bold;
}
</style>
</head>
<body>
<div class="main">
<h1 class="title">Rojo Live Sync is up and running!</h1>
<a class="docs" href="https://lpghatguy.github.io/rojo">Rojo Documentation</a>
</div>
</body>
</html>

View File

@@ -20,9 +20,14 @@ fn make_path_absolute(value: &Path) -> PathBuf {
} }
fn main() { fn main() {
env_logger::Builder::from_default_env() {
.default_format_timestamp(false) let log_env = env_logger::Env::default()
.init(); .default_filter_or("warn");
env_logger::Builder::from_env(log_env)
.default_format_timestamp(false)
.init();
}
let app = clap_app!(Rojo => let app = clap_app!(Rojo =>
(version: env!("CARGO_PKG_VERSION")) (version: env!("CARGO_PKG_VERSION"))

View File

@@ -10,7 +10,7 @@ use failure::Fail;
use crate::{ use crate::{
rbx_session::construct_oneoff_tree, rbx_session::construct_oneoff_tree,
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
imfs::Imfs, imfs::{Imfs, FsError},
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -55,14 +55,18 @@ pub enum BuildError {
XmlModelEncodeError(rbx_xml::EncodeError), XmlModelEncodeError(rbx_xml::EncodeError),
#[fail(display = "Binary model file error")] #[fail(display = "Binary model file error")]
BinaryModelEncodeError(rbx_binary::EncodeError) BinaryModelEncodeError(rbx_binary::EncodeError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(BuildError { impl_from!(BuildError {
ProjectLoadFuzzyError => ProjectLoadError, ProjectLoadFuzzyError => ProjectLoadError,
io::Error => IoError, io::Error => IoError,
rbx_xml::EncodeError => XmlModelEncodeError, rbx_xml::EncodeError => XmlModelEncodeError,
rbx_binary::EncodeError => BinaryModelEncodeError rbx_binary::EncodeError => BinaryModelEncodeError,
FsError => FsError,
}); });
pub fn build(options: &BuildOptions) -> Result<(), BuildError> { pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
@@ -75,6 +79,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Project::load_fuzzy(&options.fuzzy_project_path)?; let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);

View File

@@ -9,6 +9,7 @@ use failure::Fail;
use crate::{ use crate::{
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
web::Server, web::Server,
imfs::FsError,
live_session::LiveSession, live_session::LiveSession,
}; };
@@ -24,21 +25,26 @@ pub struct ServeOptions {
pub enum ServeError { pub enum ServeError {
#[fail(display = "Project load error: {}", _0)] #[fail(display = "Project load error: {}", _0)]
ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError), ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(ServeError { impl_from!(ServeError {
ProjectLoadFuzzyError => ProjectLoadError, ProjectLoadFuzzyError => ProjectLoadError,
FsError => FsError,
}); });
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> { pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?); let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?);
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);
let live_session = Arc::new(LiveSession::new(Arc::clone(&project)).unwrap()); let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
let server = Server::new(Arc::clone(&live_session)); let server = Server::new(Arc::clone(&live_session));
let port = options.port let port = options.port

View File

@@ -11,7 +11,7 @@ use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
use crate::{ use crate::{
rbx_session::construct_oneoff_tree, rbx_session::construct_oneoff_tree,
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
imfs::Imfs, imfs::{Imfs, FsError},
}; };
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
@@ -33,6 +33,9 @@ pub enum UploadError {
#[fail(display = "XML model file error")] #[fail(display = "XML model file error")]
XmlModelEncodeError(rbx_xml::EncodeError), XmlModelEncodeError(rbx_xml::EncodeError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(UploadError { impl_from!(UploadError {
@@ -40,6 +43,7 @@ impl_from!(UploadError {
io::Error => IoError, io::Error => IoError,
reqwest::Error => HttpError, reqwest::Error => HttpError,
rbx_xml::EncodeError => XmlModelEncodeError, rbx_xml::EncodeError => XmlModelEncodeError,
FsError => FsError,
}); });
#[derive(Debug)] #[derive(Debug)]
@@ -56,6 +60,7 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Project::load_fuzzy(&options.fuzzy_project_path)?; let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);

View File

@@ -1,10 +1,12 @@
use std::{ use std::{
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
time::Duration, time::Duration,
path::Path,
ops::Deref,
thread, thread,
}; };
use log::trace; use log::{warn, trace};
use notify::{ use notify::{
self, self,
DebouncedEvent, DebouncedEvent,
@@ -20,7 +22,66 @@ use crate::{
const WATCH_TIMEOUT: Duration = Duration::from_millis(100); const WATCH_TIMEOUT: Duration = Duration::from_millis(100);
fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: DebouncedEvent) { /// Watches for changes on the filesystem and links together the in-memory
/// filesystem and in-memory Roblox tree.
pub struct FsWatcher {
watcher: RecommendedWatcher,
}
impl FsWatcher {
/// Start a new FS watcher, watching all of the roots currently attached to
/// the given Imfs.
///
/// `rbx_session` is optional to make testing easier. If it isn't `None`,
/// events will be passed to it after they're given to the Imfs.
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Option<Arc<Mutex<RbxSession>>>) -> FsWatcher {
let (watch_tx, watch_rx) = mpsc::channel();
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
.expect("Could not create filesystem watcher");
{
let imfs = imfs.lock().unwrap();
for root_path in imfs.get_roots() {
watcher.watch(root_path, RecursiveMode::Recursive)
.expect("Could not watch directory");
}
}
{
let imfs = Arc::clone(&imfs);
let rbx_session = rbx_session.as_ref().map(Arc::clone);
thread::spawn(move || {
trace!("Watcher thread started");
while let Ok(event) = watch_rx.recv() {
// handle_fs_event expects an Option<&Mutex<T>>, but we have
// an Option<Arc<Mutex<T>>>, so we coerce with Deref.
let session_ref = rbx_session.as_ref().map(Deref::deref);
handle_fs_event(&imfs, session_ref, event);
}
trace!("Watcher thread stopped");
});
}
FsWatcher {
watcher,
}
}
pub fn stop_watching_path(&mut self, path: &Path) {
match self.watcher.unwatch(path) {
Ok(_) => {},
Err(e) => {
warn!("Could not unwatch path {}: {}", path.display(), e);
},
}
}
}
fn handle_fs_event(imfs: &Mutex<Imfs>, rbx_session: Option<&Mutex<RbxSession>>, event: DebouncedEvent) {
match event { match event {
DebouncedEvent::Create(path) => { DebouncedEvent::Create(path) => {
trace!("Path created: {}", path.display()); trace!("Path created: {}", path.display());
@@ -30,7 +91,7 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
imfs.path_created(&path).unwrap(); imfs.path_created(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_created(&path); rbx_session.path_created(&path);
} }
@@ -43,7 +104,7 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
imfs.path_updated(&path).unwrap(); imfs.path_updated(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_updated(&path); rbx_session.path_updated(&path);
} }
@@ -56,20 +117,20 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
imfs.path_removed(&path).unwrap(); imfs.path_removed(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_removed(&path); rbx_session.path_removed(&path);
} }
}, },
DebouncedEvent::Rename(from_path, to_path) => { DebouncedEvent::Rename(from_path, to_path) => {
trace!("Path rename: {} to {}", from_path.display(), to_path.display()); trace!("Path renamed: {} to {}", from_path.display(), to_path.display());
{ {
let mut imfs = imfs.lock().unwrap(); let mut imfs = imfs.lock().unwrap();
imfs.path_moved(&from_path, &to_path).unwrap(); imfs.path_moved(&from_path, &to_path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_renamed(&from_path, &to_path); rbx_session.path_renamed(&from_path, &to_path);
} }
@@ -78,49 +139,4 @@ fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: Debo
trace!("Unhandled FS event: {:?}", other); trace!("Unhandled FS event: {:?}", other);
}, },
} }
}
/// Watches for changes on the filesystem and links together the in-memory
/// filesystem and in-memory Roblox tree.
pub struct FsWatcher {
#[allow(unused)]
watchers: Vec<RecommendedWatcher>,
}
impl FsWatcher {
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Arc<Mutex<RbxSession>>) -> FsWatcher {
let mut watchers = Vec::new();
{
let imfs_temp = imfs.lock().unwrap();
for root_path in imfs_temp.get_roots() {
let (watch_tx, watch_rx) = mpsc::channel();
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
.expect("Could not create `notify` watcher");
watcher.watch(root_path, RecursiveMode::Recursive)
.expect("Could not watch directory");
watchers.push(watcher);
let imfs = Arc::clone(&imfs);
let rbx_session = Arc::clone(&rbx_session);
let root_path = root_path.clone();
thread::spawn(move || {
trace!("Watcher thread ({}) started", root_path.display());
while let Ok(event) = watch_rx.recv() {
handle_event(&imfs, &rbx_session, event);
}
trace!("Watcher thread ({}) stopped", root_path.display());
});
}
}
FsWatcher {
watchers,
}
}
} }

View File

@@ -1,15 +1,41 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
fmt,
fs, fs,
io, io,
}; };
use failure::Fail;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use crate::project::{Project, ProjectNode}; use crate::project::{Project, ProjectNode};
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> io::Result<()> { /// A wrapper around io::Error that also attaches the path associated with the
/// error.
#[derive(Debug, Fail)]
pub struct FsError {
#[fail(cause)]
inner: io::Error,
path: PathBuf,
}
impl FsError {
fn new<P: Into<PathBuf>>(inner: io::Error, path: P) -> FsError {
FsError {
inner,
path: path.into(),
}
}
}
impl fmt::Display for FsError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
write!(output, "{}: {}", self.path.display(), self.inner)
}
}
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> Result<(), FsError> {
match project_node { match project_node {
ProjectNode::Instance(node) => { ProjectNode::Instance(node) => {
for child in node.children.values() { for child in node.children.values() {
@@ -44,7 +70,7 @@ impl Imfs {
} }
} }
pub fn add_roots_from_project(&mut self, project: &Project) -> io::Result<()> { pub fn add_roots_from_project(&mut self, project: &Project) -> Result<(), FsError> {
add_sync_points(self, &project.tree) add_sync_points(self, &project.tree)
} }
@@ -63,7 +89,7 @@ impl Imfs {
self.items.get(path) self.items.get(path)
} }
pub fn add_root(&mut self, path: &Path) -> io::Result<()> { pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(!self.is_within_roots(path)); debug_assert!(!self.is_within_roots(path));
@@ -72,21 +98,33 @@ impl Imfs {
self.descend_and_read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_created(&mut self, path: &Path) -> io::Result<()> { pub fn remove_root(&mut self, path: &Path) {
debug_assert!(path.is_absolute());
if self.roots.get(path).is_some() {
self.remove_item(path);
if let Some(parent_path) = path.parent() {
self.unlink_child(parent_path, path);
}
}
}
pub fn path_created(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
self.descend_and_read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_updated(&mut self, path: &Path) -> io::Result<()> { pub fn path_updated(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
self.descend_and_read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_removed(&mut self, path: &Path) -> io::Result<()> { pub fn path_removed(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
@@ -99,7 +137,7 @@ impl Imfs {
Ok(()) Ok(())
} }
pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> io::Result<()> { pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> Result<(), FsError> {
self.path_removed(from_path)?; self.path_removed(from_path)?;
self.path_created(to_path)?; self.path_created(to_path)?;
Ok(()) Ok(())
@@ -149,9 +187,9 @@ impl Imfs {
} }
} }
fn descend_and_read_from_disk(&mut self, path: &Path) -> io::Result<()> { fn descend_and_read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
let root_path = self.get_root_path(path) let root_path = self.get_root_path(path)
.expect("Tried to mkdirp for path that wasn't within roots!"); .expect("Tried to descent and read for path that wasn't within roots!");
// If this path is a root, we should read the entire thing. // If this path is a root, we should read the entire thing.
if root_path == path { if root_path == path {
@@ -170,7 +208,6 @@ impl Imfs {
if self.items.contains_key(&next_path) { if self.items.contains_key(&next_path) {
current_path = next_path; current_path = next_path;
} else { } else {
self.read_from_disk(&current_path)?;
break; break;
} }
}, },
@@ -178,14 +215,16 @@ impl Imfs {
} }
} }
Ok(()) self.read_from_disk(&current_path)
} }
fn read_from_disk(&mut self, path: &Path) -> io::Result<()> { fn read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
let metadata = fs::metadata(path)?; let metadata = fs::metadata(path)
.map_err(|e| FsError::new(e, path))?;
if metadata.is_file() { if metadata.is_file() {
let contents = fs::read(path)?; let contents = fs::read(path)
.map_err(|e| FsError::new(e, path))?;
let item = ImfsItem::File(ImfsFile { let item = ImfsItem::File(ImfsFile {
path: path.to_path_buf(), path: path.to_path_buf(),
contents, contents,
@@ -206,8 +245,13 @@ impl Imfs {
self.items.insert(path.to_path_buf(), item); self.items.insert(path.to_path_buf(), item);
for entry in fs::read_dir(path)? { let dir_children = fs::read_dir(path)
let entry = entry?; .map_err(|e| FsError::new(e, path))?;
for entry in dir_children {
let entry = entry
.map_err(|e| FsError::new(e, path))?;
let child_path = entry.path(); let child_path = entry.path();
self.read_from_disk(&child_path)?; self.read_from_disk(&child_path)?;

View File

@@ -1,16 +1,15 @@
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
io,
}; };
use crate::{ use crate::{
fs_watcher::FsWatcher,
imfs::{Imfs, FsError},
message_queue::MessageQueue, message_queue::MessageQueue,
project::Project, project::Project,
imfs::Imfs,
session_id::SessionId,
rbx_session::RbxSession, rbx_session::RbxSession,
session_id::SessionId,
snapshot_reconciler::InstanceChanges, snapshot_reconciler::InstanceChanges,
fs_watcher::FsWatcher,
}; };
/// Contains all of the state for a Rojo live-sync session. /// Contains all of the state for a Rojo live-sync session.
@@ -24,7 +23,7 @@ pub struct LiveSession {
} }
impl LiveSession { impl LiveSession {
pub fn new(project: Arc<Project>) -> io::Result<LiveSession> { pub fn new(project: Arc<Project>) -> Result<LiveSession, FsError> {
let imfs = { let imfs = {
let mut imfs = Imfs::new(); let mut imfs = Imfs::new();
imfs.add_roots_from_project(&project)?; imfs.add_roots_from_project(&project)?;
@@ -41,7 +40,7 @@ impl LiveSession {
let fs_watcher = FsWatcher::start( let fs_watcher = FsWatcher::start(
Arc::clone(&imfs), Arc::clone(&imfs),
Arc::clone(&rbx_session), Some(Arc::clone(&rbx_session)),
); );
let session_id = SessionId::new(); let session_id = SessionId::new();

View File

@@ -6,12 +6,14 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::warn;
use failure::Fail; use failure::Fail;
use maplit::hashmap; use maplit::hashmap;
use rbx_tree::RbxValue; use rbx_tree::RbxValue;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
pub static PROJECT_FILENAME: &'static str = "roblox-project.json"; pub static PROJECT_FILENAME: &'static str = "default.project.json";
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
// Methods used for Serde's default value system, which doesn't support using // Methods used for Serde's default value system, which doesn't support using
// value literals directly, only functions that return values. // value literals directly, only functions that return values.
@@ -362,11 +364,17 @@ impl Project {
} else if location_metadata.is_dir() { } else if location_metadata.is_dir() {
let with_file = start_location.join(PROJECT_FILENAME); let with_file = start_location.join(PROJECT_FILENAME);
if let Ok(with_file_metadata) = fs::metadata(&with_file) { if let Ok(file_metadata) = fs::metadata(&with_file) {
if with_file_metadata.is_file() { if file_metadata.is_file() {
return Some(with_file); return Some(with_file);
} else { }
return None; }
let with_compat_file = start_location.join(COMPAT_PROJECT_FILENAME);
if let Ok(file_metadata) = fs::metadata(&with_compat_file) {
if file_metadata.is_file() {
return Some(with_compat_file);
} }
} }
} }
@@ -405,6 +413,25 @@ impl Project {
Ok(()) Ok(())
} }
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
pub fn check_compatibility(&self) {
let file_name = self.file_location
.file_name().unwrap()
.to_str().expect("Project file path was not valid Unicode!");
if file_name == COMPAT_PROJECT_FILENAME {
warn!("Rojo's default project file name changed in 0.5.0-alpha3.");
warn!("Support for the old project file name will be dropped before 0.5.0 releases.");
warn!("Your project file is named {}", COMPAT_PROJECT_FILENAME);
warn!("Rename your project file to {}", PROJECT_FILENAME);
} else if !file_name.ends_with(".project.json") {
warn!("Starting in Rojo 0.5.0-alpha3, it's recommended to give all project files the");
warn!(".project.json extension. This helps Rojo differentiate project files from");
warn!("other JSON files!");
}
}
fn to_source_project(&self) -> SourceProject { fn to_source_project(&self) -> SourceProject {
SourceProject { SourceProject {
name: self.name.clone(), name: self.name.clone(),

View File

@@ -5,6 +5,7 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use log::warn;
use rbx_tree::RbxId; use rbx_tree::RbxId;
use crate::{ use crate::{
@@ -27,13 +28,21 @@ digraph RojoTree {
"#; "#;
/// Compiles DOT source to SVG by invoking dot on the command line. /// Compiles DOT source to SVG by invoking dot on the command line.
pub fn graphviz_to_svg(source: &str) -> String { pub fn graphviz_to_svg(source: &str) -> Option<String> {
let mut child = Command::new("dot") let command = Command::new("dot")
.arg("-Tsvg") .arg("-Tsvg")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn() .spawn();
.expect("Failed to spawn GraphViz process -- make sure it's installed in order to use /api/visualize");
let mut child = match command {
Ok(child) => child,
Err(_) => {
warn!("Failed to spawn GraphViz process to visualize current state.");
warn!("If you want pretty graphs, install GraphViz and make sure 'dot' is on your PATH!");
return None;
},
};
{ {
let stdin = child.stdin.as_mut().expect("Failed to open stdin"); let stdin = child.stdin.as_mut().expect("Failed to open stdin");
@@ -41,7 +50,7 @@ pub fn graphviz_to_svg(source: &str) -> String {
} }
let output = child.wait_with_output().expect("Failed to read stdout"); let output = child.wait_with_output().expect("Failed to read stdout");
String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8") Some(String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8"))
} }
/// A Display wrapper struct to visualize an RbxSession as SVG. /// A Display wrapper struct to visualize an RbxSession as SVG.

View File

@@ -22,6 +22,8 @@ use crate::{
rbx_session::{MetadataPerInstance}, rbx_session::{MetadataPerInstance},
}; };
static HOME_CONTENT: &str = include_str!("../assets/index.html");
/// Contains the instance metadata relevant to Rojo clients. /// Contains the instance metadata relevant to Rojo clients.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -96,136 +98,31 @@ impl Server {
router!(request, router!(request,
(GET) (/) => { (GET) (/) => {
Response::text("Rojo is up and running!") self.handle_home()
}, },
(GET) (/api/rojo) => { (GET) (/api/rojo) => {
// Get a summary of information about the server. self.handle_api_rojo()
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
Response::json(&ServerInfoResponse {
server_version: self.server_version,
protocol_version: 2,
session_id: self.live_session.session_id,
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
root_instance_id: tree.get_root_id(),
})
}, },
(GET) (/api/subscribe/{ cursor: u32 }) => { (GET) (/api/subscribe/{ cursor: u32 }) => {
// Retrieve any messages past the given cursor index, and if self.handle_api_subscribe(cursor)
// there weren't any, subscribe to receive any new messages.
let message_queue = Arc::clone(&self.live_session.message_queue);
// Did the client miss any messages since the last subscribe?
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
if !new_messages.is_empty() {
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Borrowed(&new_messages),
message_cursor: new_cursor,
})
}
}
let (tx, rx) = mpsc::channel();
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::text("error!").with_status_code(500),
}
message_queue.unsubscribe(sender_id);
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Owned(new_messages),
message_cursor: new_cursor,
})
}
}, },
(GET) (/api/read/{ id_list: String }) => { (GET) (/api/read/{ id_list: String }) => {
let message_queue = Arc::clone(&self.live_session.message_queue);
let requested_ids: Option<Vec<RbxId>> = id_list let requested_ids: Option<Vec<RbxId>> = id_list
.split(',') .split(',')
.map(RbxId::parse_str) .map(RbxId::parse_str)
.collect(); .collect();
let requested_ids = match requested_ids { self.handle_api_read(requested_ids)
Some(id) => id,
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
};
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
let message_cursor = message_queue.get_message_cursor();
let mut instances = HashMap::new();
for &requested_id in &requested_ids {
if let Some(instance) = tree.get_instance(requested_id) {
let metadata = rbx_session.get_instance_metadata(requested_id)
.map(InstanceMetadata::from_session_metadata);
instances.insert(instance.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(instance),
metadata,
});
for descendant in tree.descendants(requested_id) {
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
.map(InstanceMetadata::from_session_metadata);
instances.insert(descendant.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(descendant),
metadata: descendant_meta,
});
}
}
}
Response::json(&ReadResponse {
session_id: self.live_session.session_id,
message_cursor,
instances,
})
}, },
(GET) (/visualize/rbx) => { (GET) (/visualize/rbx) => {
let rbx_session = self.live_session.rbx_session.lock().unwrap(); self.handle_visualize_rbx()
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
Response::svg(graphviz_to_svg(&dot_source))
}, },
(GET) (/visualize/imfs) => { (GET) (/visualize/imfs) => {
let imfs = self.live_session.imfs.lock().unwrap(); self.handle_visualize_imfs()
let dot_source = format!("{}", VisualizeImfs(&imfs));
Response::svg(graphviz_to_svg(&dot_source))
}, },
(GET) (/visualize/path_metadata) => { (GET) (/visualize/path_metadata) => {
let rbx_session = self.live_session.rbx_session.lock().unwrap(); self.handle_visualize_path_metadata()
Response::json(&rbx_session.debug_get_metadata_per_path())
}, },
_ => Response::empty_404() _ => Response::empty_404()
) )
} }
@@ -235,4 +132,131 @@ impl Server {
rouille::start_server(address, move |request| self.handle_request(request)); rouille::start_server(address, move |request| self.handle_request(request));
} }
fn handle_home(&self) -> Response {
Response::html(HOME_CONTENT)
}
/// Get a summary of information about the server
fn handle_api_rojo(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
Response::json(&ServerInfoResponse {
server_version: self.server_version,
protocol_version: 2,
session_id: self.live_session.session_id,
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
root_instance_id: tree.get_root_id(),
})
}
/// Retrieve any messages past the given cursor index, and if
/// there weren't any, subscribe to receive any new messages.
fn handle_api_subscribe(&self, cursor: u32) -> Response {
let message_queue = Arc::clone(&self.live_session.message_queue);
// Did the client miss any messages since the last subscribe?
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
if !new_messages.is_empty() {
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Borrowed(&new_messages),
message_cursor: new_cursor,
})
}
}
let (tx, rx) = mpsc::channel();
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::text("error!").with_status_code(500),
}
message_queue.unsubscribe(sender_id);
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Owned(new_messages),
message_cursor: new_cursor,
})
}
}
fn handle_api_read(&self, requested_ids: Option<Vec<RbxId>>) -> Response {
let message_queue = Arc::clone(&self.live_session.message_queue);
let requested_ids = match requested_ids {
Some(id) => id,
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
};
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
let message_cursor = message_queue.get_message_cursor();
let mut instances = HashMap::new();
for &requested_id in &requested_ids {
if let Some(instance) = tree.get_instance(requested_id) {
let metadata = rbx_session.get_instance_metadata(requested_id)
.map(InstanceMetadata::from_session_metadata);
instances.insert(instance.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(instance),
metadata,
});
for descendant in tree.descendants(requested_id) {
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
.map(InstanceMetadata::from_session_metadata);
instances.insert(descendant.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(descendant),
metadata: descendant_meta,
});
}
}
}
Response::json(&ReadResponse {
session_id: self.live_session.session_id,
message_cursor,
instances,
})
}
fn handle_visualize_rbx(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
match graphviz_to_svg(&dot_source) {
Some(svg) => Response::svg(svg),
None => Response::text(dot_source),
}
}
fn handle_visualize_imfs(&self) -> Response {
let imfs = self.live_session.imfs.lock().unwrap();
let dot_source = format!("{}", VisualizeImfs(&imfs));
match graphviz_to_svg(&dot_source) {
Some(svg) => Response::svg(svg),
None => Response::text(dot_source),
}
}
fn handle_visualize_path_metadata(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
Response::json(&rbx_session.debug_get_metadata_per_path())
}
} }

View File

@@ -1,10 +1,10 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
io,
fs, fs,
path::PathBuf, path::PathBuf,
}; };
use failure::Error;
use tempfile::{TempDir, tempdir}; use tempfile::{TempDir, tempdir};
use librojo::{ use librojo::{
@@ -19,7 +19,7 @@ enum FsEvent {
Moved(PathBuf, PathBuf), Moved(PathBuf, PathBuf),
} }
fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> io::Result<()> { fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> Result<(), Error> {
for event in events { for event in events {
match event { match event {
FsEvent::Created(path) => imfs.path_created(path)?, FsEvent::Created(path) => imfs.path_created(path)?,
@@ -56,7 +56,7 @@ fn check_expected(real: &Imfs, expected: &ExpectedImfs) {
} }
} }
fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> { fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> {
let root = tempdir()?; let root = tempdir()?;
let foo_path = root.path().join("foo"); let foo_path = root.path().join("foo");
@@ -125,7 +125,7 @@ fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> {
} }
#[test] #[test]
fn initial_read() -> io::Result<()> { fn initial_read() -> Result<(), Error> {
let (_root, imfs, expected_imfs, _resources) = base_tree()?; let (_root, imfs, expected_imfs, _resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -134,7 +134,7 @@ fn initial_read() -> io::Result<()> {
} }
#[test] #[test]
fn adding_files() -> io::Result<()> { fn adding_files() -> Result<(), Error> {
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?; let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -178,7 +178,7 @@ fn adding_files() -> io::Result<()> {
} }
#[test] #[test]
fn adding_folder() -> io::Result<()> { fn adding_folder() -> Result<(), Error> {
let (root, imfs, mut expected_imfs, _resources) = base_tree()?; let (root, imfs, mut expected_imfs, _resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -255,7 +255,36 @@ fn adding_folder() -> io::Result<()> {
} }
#[test] #[test]
fn removing_file() -> io::Result<()> { fn updating_files() -> Result<(), Error> {
let (_root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs);
fs::write(&resources.bar_path, b"bar updated")?;
fs::write(&resources.baz_path, b"baz updated")?;
imfs.path_updated(&resources.bar_path)?;
imfs.path_updated(&resources.baz_path)?;
let bar_updated_item = ImfsItem::File(ImfsFile {
path: resources.bar_path.clone(),
contents: b"bar updated".to_vec(),
});
let baz_updated_item = ImfsItem::File(ImfsFile {
path: resources.baz_path.clone(),
contents: b"baz updated".to_vec(),
});
expected_imfs.items.insert(resources.bar_path.clone(), bar_updated_item);
expected_imfs.items.insert(resources.baz_path.clone(), baz_updated_item);
check_expected(&imfs, &expected_imfs);
Ok(())
}
#[test]
fn removing_file() -> Result<(), Error> {
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?; let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -279,7 +308,7 @@ fn removing_file() -> io::Result<()> {
} }
#[test] #[test]
fn removing_folder() -> io::Result<()> { fn removing_folder() -> Result<(), Error> {
let (root, imfs, mut expected_imfs, resources) = base_tree()?; let (root, imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);

View File

@@ -21,7 +21,7 @@ lazy_static! {
#[test] #[test]
fn empty() { fn empty() {
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "empty"); assert_eq!(project.name, "empty");
@@ -29,7 +29,7 @@ fn empty() {
#[test] #[test]
fn empty_fuzzy_file() { fn empty_fuzzy_file() {
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
let project = Project::load_fuzzy(&project_file_location).unwrap(); let project = Project::load_fuzzy(&project_file_location).unwrap();
assert_eq!(project.name, "empty"); assert_eq!(project.name, "empty");
@@ -45,7 +45,7 @@ fn empty_fuzzy_folder() {
#[test] #[test]
fn single_sync_point() { fn single_sync_point() {
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
let expected_project = { let expected_project = {
@@ -100,7 +100,7 @@ fn single_sync_point() {
#[test] #[test]
fn test_model() { fn test_model() {
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("test-model/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "test-model"); assert_eq!(project.name, "test-model");

View File

@@ -0,0 +1,6 @@
{
"name": "missing-files",
"tree": {
"$path": "does-not-exist"
}
}