Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions documentation/specs/proposed/partial-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,18 @@ public class ProjectOptions
}
```

The stage flows through the existing factory methods:
The stage flows through the `ProjectInstance` factory methods:

- `ProjectInstance.FromFile(path, options)` / `ProjectInstance.FromProjectRootElement(xml, options)`
- `Project.FromFile(path, options)` / `Project.FromProjectRootElement(xml, options)` / `Project.FromXmlReader(reader, options)`

Both `Project.EvaluationStage` and `ProjectInstance.EvaluationStage` report the stage the object was
evaluated to.
`ProjectInstance.EvaluationStage` reports the stage the instance was evaluated to.

Partial evaluation is intentionally **not** supported on the mutable `Project` object model. `Project`
is an editable model that is cached in the `ProjectCollection`; a partially-evaluated `Project` would
either serve stale state from that cache or be silently upgraded to a full evaluation, discarding the
work the partial evaluation saved. The `Project` factory methods (`Project.FromFile`,
`Project.FromProjectRootElement`, `Project.FromXmlReader`) therefore throw `ArgumentException` when a
non-`Full` `EvaluationStage` is requested; use `ProjectInstance` for partial evaluation.

Example:

Expand All @@ -81,27 +86,22 @@ string value = instance.GetPropertyValue("PublishRelease"); // fast: only passes
- **Properties are always valid** for any stage ≥ `Properties` (`GetProperty`, `GetPropertyValue`,
`Properties`, `GlobalProperties`). `InitialTargets` is also available from `Properties` onward
because it is computed during pass 1.
- **Reading not-yet-computed state fails fast.** Members that expose state from a later pass throw
`InvalidOperationException` naming the member and the stage the object reached. Guarded members
include `ItemDefinitions` (available from `ItemDefinitions` onward), `Items`, `GetItems`,
`ItemsIgnoringCondition`, `AllEvaluatedItems`, `Targets`, and `DefaultTargets` (and their
`ProjectInstance` equivalents).
- **Reading not-yet-computed state fails fast.** `ProjectInstance` members that expose state from a
later pass throw `InvalidOperationException` naming the member and the stage the instance reached.
Guarded members include `ItemDefinitions` (available from `ItemDefinitions` onward), `Items`,
`GetItems`, `Targets`, and `DefaultTargets`.
- **A partial `ProjectInstance` cannot be built.** Constructing a `BuildRequestData` from a partial
instance throws `InvalidOperationException`; a build requires a full evaluation.

The default (`Full`) is unchanged, so existing callers are unaffected.

## Evaluation caching

`ProjectCollection` caches loaded `Project`s keyed on (path, global properties, tools version) — the
evaluation stage is not part of the key. To avoid serving stale partial state:

- A cached project satisfies a request only if `cachedStage >= requestedStage`.
- `ProjectCollection.LoadProject` requests `Full`. If the only cached project for a key was
partially evaluated, it is **upgraded in place** (re-evaluated to `Full`) and returned, rather than
returning partial state or creating a duplicate cache entry.
- Calling the public `Project.ReevaluateIfNecessary()` on a partial project upgrades it to `Full`
(a partial evaluation leaves the project non-dirty, so the re-evaluation is forced).
Partial evaluation applies only to `ProjectInstance`, which is an immutable evaluation snapshot that
is **not** stored in the `ProjectCollection` loaded-project cache. A partial `ProjectInstance` can
therefore never be returned in place of a fully-evaluated object for a later `Full` request, so there
is no cache-staleness or upgrade-in-place concern. This is a primary reason partial evaluation is
limited to `ProjectInstance`: the cached, mutable `Project` model cannot expose partial state safely.

## Relationship to `EvaluationContext`

Expand Down
85 changes: 25 additions & 60 deletions src/Build.UnitTests/Evaluation/PartialEvaluation_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,34 @@ public void PropertiesStage_ExposesPropertiesButNotItemsOrTargets_ProjectInstanc
}

[Fact]
public void PropertiesStage_ExposesPropertiesButNotItemsOrTargets_Project()
public void ProjectFactories_RejectPartialEvaluationStage()
{
Project project = Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Properties));
// Project only supports full evaluation. A partial stage passed through ProjectOptions
// must be rejected up front rather than silently ignored.
Should.Throw<ArgumentException>(() =>
Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Properties)));

project.EvaluationStage.ShouldBe(ProjectEvaluationStage.Properties);
project.GetPropertyValue("Derived").ShouldBe("Debug-x");
Should.Throw<ArgumentException>(() =>
Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Items)));
}

Should.Throw<InvalidOperationException>(() => project.Items);
Should.Throw<InvalidOperationException>(() => project.ItemDefinitions);
Should.Throw<InvalidOperationException>(() => project.GetItems("Compile"));
Should.Throw<InvalidOperationException>(() => project.AllEvaluatedItems);
Should.Throw<InvalidOperationException>(() => project.Targets);
[Fact]
public void ProjectFactories_RejectNullOptions()
{
// The partial-stage guard is the first thing the factories do, so it must not dereference
// a null ProjectOptions; callers get the canonical ArgumentNullException instead.
Should.Throw<ArgumentNullException>(() =>
Project.FromProjectRootElement(CreateRootElement(), null));
}

[Fact]
public void ProjectFactories_AllowFullEvaluationStage()
{
Project project = Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Full));

project.GetPropertyValue("Derived").ShouldBe("Debug-x");
project.GetItems("Compile").Count.ShouldBe(2);
project.Targets.ShouldContainKey("Build");
}

[Fact]
Expand Down Expand Up @@ -165,63 +181,12 @@ public void FullStage_IsDefault_AndExposesEverything()
instance.Targets.ShouldContainKey("Build");
}

[Fact]
public void ReevaluateUpgradesPartialProjectToFull()
{
Project project = Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Properties));

project.EvaluationStage.ShouldBe(ProjectEvaluationStage.Properties);
Should.Throw<InvalidOperationException>(() => project.Targets);

project.ReevaluateIfNecessary();

project.EvaluationStage.ShouldBe(ProjectEvaluationStage.Full);
project.Targets.ShouldContainKey("Build");
project.GetItems("Compile").Count.ShouldBe(2);
}

[Fact]
public void CreateProjectInstanceFromPartialProjectUpgradesToFull()
{
Project project = Project.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Properties));
project.EvaluationStage.ShouldBe(ProjectEvaluationStage.Properties);

ProjectInstance instance = project.CreateProjectInstance();

instance.EvaluationStage.ShouldBe(ProjectEvaluationStage.Full);
instance.Targets.ShouldContainKey("Build");
project.EvaluationStage.ShouldBe(ProjectEvaluationStage.Full);
}

[Fact]
public void PartialProjectInstanceCannotBeBuilt()
{
ProjectInstance instance = ProjectInstance.FromProjectRootElement(CreateRootElement(), OptionsFor(ProjectEvaluationStage.Properties));

Should.Throw<InvalidOperationException>(() => new BuildRequestData(instance, new[] { "Build" }));
}

[Fact]
public void CacheDoesNotServePartialProjectForFullLoad()
{
TransientTestFile file = _env.CreateFile("test.proj", ProjectXml);

Project partial = Project.FromFile(file.Path, new ProjectOptions
{
EvaluationStage = ProjectEvaluationStage.Properties,
ProjectCollection = _collection,
});
partial.EvaluationStage.ShouldBe(ProjectEvaluationStage.Properties);

// A subsequent full LoadProject on the same key must return a fully-evaluated project
// (the cached partial one is upgraded in place), never partial state.
Project full = _collection.LoadProject(file.Path);
full.EvaluationStage.ShouldBe(ProjectEvaluationStage.Full);
full.Targets.ShouldContainKey("Build");

// The partial and full references point at the same upgraded cached project.
ReferenceEquals(partial, full).ShouldBeTrue();
partial.EvaluationStage.ShouldBe(ProjectEvaluationStage.Full);
}
}
}
Loading
Loading