Roll-forward policy changes (dotnet/core-setup#3537)
authorSteve Harter <steveharter@users.noreply.github.com>
Tue, 19 Dec 2017 17:12:05 +0000 (11:12 -0600)
committerGitHub <noreply@github.com>
Tue, 19 Dec 2017 17:12:05 +0000 (11:12 -0600)
Roll-forward policy changes:

Automatically roll-forward on [minor] if targeted version is not installed (previously off by default)

Automatically roll-forward from release->preview on [minor] if there is no viable release version (previously not possible)

Extend the roll-forward-on-no-candidate-fx setting from two to three values:
0 = Off.
1 = Roll forward on minor
2 = Roll forward on major and minor (previously this was the semantics for value >= 1)

Commit migrated from https://github.com/dotnet/core-setup/commit/5a1a53181b51c00b34c9b2fb8be92d6f03b5e9ed

src/installer/corehost/cli/fxr/fx_muxer.cpp
src/installer/corehost/cli/fxr/fx_muxer.h
src/installer/corehost/cli/runtime_config.cpp
src/installer/corehost/cli/runtime_config.h
src/installer/test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs

index ba6da20..7717cb6 100644 (file)
@@ -500,29 +500,64 @@ bool fx_muxer_t::resolve_hostpolicy_dir(
 fx_ver_t fx_muxer_t::resolve_framework_version(const std::vector<fx_ver_t>& version_list,
     const pal::string_t& fx_ver,
     const fx_ver_t& specified,
-    const bool& patch_roll_fwd,
-    const int& roll_fwd_on_no_candidate_fx)
+    bool patch_roll_fwd,
+    roll_fwd_on_no_candidate_fx_option roll_fwd_on_no_candidate_fx)
 {
     trace::verbose(_X("Attempting FX roll forward starting from [%s]"), fx_ver.c_str());
 
     fx_ver_t most_compatible = specified;
     if (!specified.is_prerelease())
     {
-        if (roll_fwd_on_no_candidate_fx > 0)
+        if (roll_fwd_on_no_candidate_fx != roll_fwd_on_no_candidate_fx_option::disabled)
         {
-            trace::verbose(_X("'Roll forward on no candidate fx' enabled. Looking for the least production greater than or equal to [%s]"),
-                fx_ver.c_str());
             fx_ver_t next_lowest(-1, -1, -1);
+
+            // Look for the least production version
+            trace::verbose(_X("'Roll forward on no candidate fx' enabled with value [%d]. Looking for the least production greater than or equal to [%s]"),
+                roll_fwd_on_no_candidate_fx, fx_ver.c_str());
+
             for (const auto& ver : version_list)
             {
                 if (!ver.is_prerelease() && ver >= specified)
                 {
+                    if (roll_fwd_on_no_candidate_fx == roll_fwd_on_no_candidate_fx_option::minor)
+                    {
+                        // We only want to roll forward on minor
+                        if (ver.get_major() != specified.get_major())
+                        {
+                            continue;
+                        }
+                    }
                     next_lowest = (next_lowest == fx_ver_t(-1, -1, -1)) ? ver : std::min(next_lowest, ver);
                 }
             }
+
             if (next_lowest == fx_ver_t(-1, -1, -1))
             {
-                trace::verbose(_X("No production greater than or equal to [%s] found."), fx_ver.c_str());
+                // Look for the least preview version
+                trace::verbose(_X("No production greater than or equal to [%s] found. Looking for the least preview greater than [%s]"),
+                    fx_ver.c_str(), fx_ver.c_str());
+
+                for (const auto& ver : version_list)
+                {
+                    if (ver.is_prerelease() && ver >= specified)
+                    {
+                        if (roll_fwd_on_no_candidate_fx == roll_fwd_on_no_candidate_fx_option::minor)
+                        {
+                            // We only want to roll forward on minor
+                            if (ver.get_major() != specified.get_major())
+                            {
+                                continue;
+                            }
+                        }
+                        next_lowest = (next_lowest == fx_ver_t(-1, -1, -1)) ? ver : std::min(next_lowest, ver);
+                    }
+                }
+            }
+
+            if (next_lowest == fx_ver_t(-1, -1, -1))
+            {
+                trace::verbose(_X("No preview greater than or equal to [%s] found."), fx_ver.c_str());
             }
             else
             {
@@ -530,6 +565,7 @@ fx_ver_t fx_muxer_t::resolve_framework_version(const std::vector<fx_ver_t>& vers
                 most_compatible = next_lowest;
             }
         }
+
         if (patch_roll_fwd)
         {
             trace::verbose(_X("Applying patch roll forward from [%s]"), most_compatible.as_str().c_str());
@@ -537,11 +573,11 @@ fx_ver_t fx_muxer_t::resolve_framework_version(const std::vector<fx_ver_t>& vers
             {
                 trace::verbose(_X("Inspecting version... [%s]"), ver.as_str().c_str());
 
-                if (!ver.is_prerelease() && //  only prod. prevents roll forward to prerelease.
+                if (most_compatible.is_prerelease() == ver.is_prerelease() && // prevent production from rolling forward to preview on patch
                     ver.get_major() == most_compatible.get_major() &&
                     ver.get_minor() == most_compatible.get_minor())
                 {
-                    // Pick the greatest production that differs only in patch.
+                    // Pick the greatest that differs only in patch.
                     most_compatible = std::max(ver, most_compatible);
                 }
             }
@@ -609,12 +645,20 @@ fx_definition_t* fx_muxer_t::resolve_fx(
     std::vector<pal::string_t> global_dirs;
     bool multilevel_lookup = multilevel_lookup_enabled();
 
-    hive_dir.push_back(own_dir);
+    // own_dir contains DIR_SEPARATOR appended that we need to remove.
+    pal::string_t own_dir_temp = own_dir;
+    remove_trailing_dir_seperator(&own_dir_temp);
+
+    hive_dir.push_back(own_dir_temp);
     if (multilevel_lookup && pal::get_global_dotnet_dirs(&global_dirs))
     {
         for (pal::string_t dir : global_dirs)
         {
-            hive_dir.push_back(dir);
+            // Avoid duplicate of own_dir_temp
+            if (dir != own_dir_temp)
+            {
+                hive_dir.push_back(dir);
+            }
         }
     }
 
@@ -636,7 +680,7 @@ fx_definition_t* fx_muxer_t::resolve_fx(
             if (!specified.is_prerelease())
             {
                 // If production and no roll forward use given version.
-                do_roll_forward = (config.get_patch_roll_fwd()) || (config.get_roll_fwd_on_no_candidate_fx() > 0);
+                do_roll_forward = (config.get_patch_roll_fwd()) || (config.get_roll_fwd_on_no_candidate_fx() != roll_fwd_on_no_candidate_fx_option::disabled);
             }
             else
             {
@@ -682,11 +726,14 @@ fx_definition_t* fx_muxer_t::resolve_fx(
 
             if (pal::directory_exists(fx_dir))
             {
-                //Continue to search for a better match if available
-                std::vector<fx_ver_t> version_list;
-                version_list.push_back(resolved_ver);
-                version_list.push_back(selected_ver);
-                resolved_ver = resolve_framework_version(version_list, fx_ver, specified, config.get_patch_roll_fwd(), config.get_roll_fwd_on_no_candidate_fx());
+                if (selected_ver != fx_ver_t(-1, -1, -1))
+                {
+                    // Compare the previous hive_dir selection with the current hive_dir to see which one is the better match
+                    std::vector<fx_ver_t> version_list;
+                    version_list.push_back(resolved_ver);
+                    version_list.push_back(selected_ver);
+                    resolved_ver = resolve_framework_version(version_list, fx_ver, specified, config.get_patch_roll_fwd(), config.get_roll_fwd_on_no_candidate_fx());
+                }
 
                 if (resolved_ver != selected_ver)
                 {
@@ -1250,15 +1297,15 @@ int fx_muxer_t::read_config_and_execute(
 
     auto config = app->get_runtime_config();
 
-    // 'Roll forward on no candidate fx' is disabled by default. It can be enabled through:
-    // 1. Command line argument (--roll-forward-on-no-candidate-fx)
-    // 2. Runtimeconfig json file ('rollForwardOnNoCandidateFx' property)
-    // 3. DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX env var
+    // 'Roll forward on no candidate fx' is set to 1 (roll_fwd_on_no_candidate_fx_option::minor) by default. It can be changed through:
+    // 1. Command line argument (--roll-forward-on-no-candidate-fx). Only defaults the app's config.
+    // 2. Runtimeconfig json file ('rollForwardOnNoCandidateFx' property), which is used as a default for lower level frameworks if they don't specify a value.
+    // 3. DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX env var. Only defaults the app's config.
     // The conflicts will be resolved by following the priority rank described above (from 1 to 3).
     // The env var condition is verified in the config file processing
     if (!roll_fwd_on_no_candidate_fx.empty())
     {
-        config.set_roll_fwd_on_no_candidate_fx(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str()));
+        config.set_roll_fwd_on_no_candidate_fx(static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str())));
     }
 
     // Determine additional deps
index f58c95b..309e061 100644 (file)
@@ -36,7 +36,7 @@ private:
         const pal::string_t& specified_fx_version,
         const std::vector<pal::string_t>& probe_realpaths,
         pal::string_t* impl_dir);
-    static fx_ver_t resolve_framework_version(const std::vector<fx_ver_t>& version_list, const pal::string_t& fx_ver, const fx_ver_t& specified, const bool& patch_roll_fwd, const int& roll_fwd_on_no_candidate_fx);
+    static fx_ver_t resolve_framework_version(const std::vector<fx_ver_t>& version_list, const pal::string_t& fx_ver, const fx_ver_t& specified, bool patch_roll_fwd, roll_fwd_on_no_candidate_fx_option roll_fwd_on_no_candidate_fx);
     static fx_definition_t* resolve_fx(
         host_mode_t mode,
         const runtime_config_t& config,
index e314607..6ed5b94 100644 (file)
@@ -11,7 +11,7 @@
 runtime_config_t::runtime_config_t()
     : m_patch_roll_fwd(true)
     , m_prerelease_roll_fwd(false)
-    , m_roll_fwd_on_no_candidate_fx(0)
+    , m_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option::minor)
     , m_portable(false)
     , m_valid(false)
 {
@@ -29,6 +29,16 @@ void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev
         m_prerelease_roll_fwd = defaults->m_prerelease_roll_fwd;
         m_roll_fwd_on_no_candidate_fx = defaults->m_roll_fwd_on_no_candidate_fx;
     }
+    else
+    {
+        // Since there is no previous config, this is the app's config, so default m_roll_fwd_on_no_candidate_fx from the env variable.
+        // The value will be overwritten during parsing if the setting exists in the config file.
+        pal::string_t env_no_candidate;
+        if (pal::getenv(_X("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"), &env_no_candidate))
+        {
+            m_roll_fwd_on_no_candidate_fx = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(env_no_candidate.c_str()));
+        }
+    }
 
     m_valid = ensure_parsed();
 
@@ -96,15 +106,7 @@ bool runtime_config_t::parse_opts(const json_value& opts)
     auto roll_fwd_on_no_candidate_fx = opts_obj.find(_X("rollForwardOnNoCandidateFx"));
     if (roll_fwd_on_no_candidate_fx != opts_obj.end())
     {
-        m_roll_fwd_on_no_candidate_fx = roll_fwd_on_no_candidate_fx->second.as_integer();
-    }
-    else
-    {
-        pal::string_t env_no_candidate;
-        if (pal::getenv(_X("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"), &env_no_candidate))
-        {
-            m_roll_fwd_on_no_candidate_fx = pal::xtoi(env_no_candidate.c_str());
-        }
+        m_roll_fwd_on_no_candidate_fx = static_cast<roll_fwd_on_no_candidate_fx_option>(roll_fwd_on_no_candidate_fx->second.as_integer());
     }
 
     auto tfm = opts_obj.find(_X("tfm"));
@@ -249,13 +251,13 @@ bool runtime_config_t::get_prerelease_roll_fwd() const
     return m_prerelease_roll_fwd;
 }
 
-int runtime_config_t::get_roll_fwd_on_no_candidate_fx() const
+roll_fwd_on_no_candidate_fx_option runtime_config_t::get_roll_fwd_on_no_candidate_fx() const
 {
     assert(m_valid);
     return m_roll_fwd_on_no_candidate_fx;
 }
 
-void runtime_config_t::set_roll_fwd_on_no_candidate_fx(int value)
+void runtime_config_t::set_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value)
 {
     assert(m_valid);
     m_roll_fwd_on_no_candidate_fx = value;
index e4072d2..32aa258 100644 (file)
 
 typedef web::json::value json_value;
 
+enum class roll_fwd_on_no_candidate_fx_option
+{
+    disabled = 0,
+    minor,
+    major_or_minor
+};
+
 class runtime_config_t
 {
 public:
@@ -26,8 +33,8 @@ public:
     const std::list<pal::string_t>& get_probe_paths() const;
     bool get_patch_roll_fwd() const;
     bool get_prerelease_roll_fwd() const;
-    int get_roll_fwd_on_no_candidate_fx() const;
-    void set_roll_fwd_on_no_candidate_fx(int value);
+    roll_fwd_on_no_candidate_fx_option get_roll_fwd_on_no_candidate_fx() const;
+    void set_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value);
     bool get_portable() const;
     bool parse_opts(const json_value& opts);
     void combine_properties(std::unordered_map<pal::string_t, pal::string_t>& combined_properties) const;
@@ -45,7 +52,7 @@ private:
     pal::string_t m_fx_ver;
     bool m_patch_roll_fwd;
     bool m_prerelease_roll_fwd;
-    int m_roll_fwd_on_no_candidate_fx;
+    roll_fwd_on_no_candidate_fx_option m_roll_fwd_on_no_candidate_fx;
 
     pal::string_t m_dev_path;
     pal::string_t m_path;
index ac29701..783be9e 100644 (file)
@@ -192,7 +192,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             // Verify we have the expected runtime versions
             dotnet.Exec("--list-runtimes")
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("COREHOST_TRACE", "1")
                 .WithUserProfile(_userDir)
                 .CaptureStdOut()
                 .Execute()
@@ -285,12 +284,12 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "10000.1.1", "10000.1.3");
 
             // Version: 9999.0.0
-            // 'Roll forward on no candidate fx' enabled through env var
+            // 'Roll forward on no candidate fx' enabled with value 2 (major+minor) through env var
             // exe: 10000.1.1, 10000.1.3
             // Expected: 10000.1.3 from exe
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "2")
                 .EnvironmentVariable("COREHOST_TRACE", "1")
                 .CaptureStdOut()
                 .CaptureStdErr()
@@ -304,12 +303,12 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1");
 
             // Version: 9999.0.0
-            // 'Roll forward on no candidate fx' enabled through env var
+            // 'Roll forward on no candidate fx' enabled with value 2 (major+minor) through env var
             // exe: 9999.1.1, 10000.1.1, 10000.1.3
             // Expected: 9999.1.1 from exe
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "2")
                 .EnvironmentVariable("COREHOST_TRACE", "1")
                 .CaptureStdOut()
                 .CaptureStdErr()
@@ -322,7 +321,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             // Verify we have the expected runtime versions
             dotnet.Exec("--list-runtimes")
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
                 .CaptureStdOut()
                 .Execute()
                 .Should()
@@ -334,7 +332,257 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
                 .And
                 .HaveStdOutContaining("Microsoft.NETCore.App 10000.1.3");
 
-            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "10000.1.1", "10000.1.3");
+            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1", "10000.1.1", "10000.1.3");
+        }
+
+        [Fact]
+        public void Roll_Forward_On_No_Candidate_Fx_Minor_And_Disabled()
+        {
+            var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture
+                .Copy();
+
+            var dotnet = fixture.BuiltDotnet;
+            var appDll = fixture.TestProject.AppDll;
+
+            // Set desired version = 9999.0.0
+            string runtimeConfig = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json");
+            SetRuntimeConfigJson(runtimeConfig, "9999.0.0");
+
+            // Add some dummy versions in the exe
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "10000.1.1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 10000.1.1
+            // Expected: fail with no framework
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Fail()
+                .And
+                .HaveStdErrContaining("It was not possible to find any compatible framework version");
+
+            // Add a dummy version in the exe dir 
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 9999.1.1, 10000.1.1
+            // Expected: 9999.1.1 from exe
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.1.1"));
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' disabled through env var
+            // exe: 9999.1.1, 10000.1.1
+            // Expected: fail with no framework
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "0")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Fail()
+                .And
+                .HaveStdErrContaining("It was not possible to find any compatible framework version");
+
+            // Verify we have the expected runtime versions
+            dotnet.Exec("--list-runtimes")
+                .WorkingDirectory(_currentWorkingDir)
+                .CaptureStdOut()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 9999.1.1")
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 10000.1.1");
+
+            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1", "10000.1.1");
+        }
+
+        [Fact]
+        public void Roll_Forward_On_No_Candidate_Fx_Production_To_Preview()
+        {
+            var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture
+                .Copy();
+
+            var dotnet = fixture.BuiltDotnet;
+            var appDll = fixture.TestProject.AppDll;
+
+            // Set desired version = 9999.0.0
+            string runtimeConfig = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json");
+            SetRuntimeConfigJson(runtimeConfig, "9999.0.0");
+
+            // Add preview version in the exe
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1-dummy1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 9999.1.1-dummy1
+            // Expected: 9999.1.1-dummy1 since there is no production version
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.1.1-dummy1"));
+
+            // Add a production version with higher value
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.2.1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 9999.1.1-dummy1, 9999.2.1
+            // Expected: 9999.2.1 since we favor production over preview
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.2.1"));
+
+            // Add a preview version with same major.minor as production
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.2.1-dummy1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 9999.1.1-dummy1, 9999.2.1, 9999.2.1-dummy1
+            // Expected: 9999.2.1 since we favor production over preview
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.2.1"));
+
+            // Add a preview version with same major.minor as production but higher patch version
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.2.2-dummy1");
+
+            // Version: 9999.0.0
+            // 'Roll forward on no candidate fx' default value of 1 (minor)
+            // exe: 9999.1.1-dummy1, 9999.2.1, 9999.2.1-dummy1, 9999.2.2-dummy1
+            // Expected: 9999.2.1 since we favor production over preview
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.2.1"));
+
+            // Verify we have the expected runtime versions
+            dotnet.Exec("--list-runtimes")
+                .WorkingDirectory(_currentWorkingDir)
+                .CaptureStdOut()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 9999.1.1-dummy1")
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 9999.2.1")
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 9999.2.1-dummy1")
+                .And
+                .HaveStdOutContaining("Microsoft.NETCore.App 9999.2.2-dummy1");
+
+            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.1-dummy1", "9999.2.1", "9999.2.1-dummy1", "9999.2.2-dummy1");
+        }
+
+        [Fact]
+        public void Roll_Forward_On_No_Candidate_Fx_Preview_To_Production()
+        {
+            var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture
+                .Copy();
+
+            var dotnet = fixture.BuiltDotnet;
+            var appDll = fixture.TestProject.AppDll;
+
+            // Set desired version = 9999.0.0-dummy1
+            string runtimeConfig = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json");
+            SetRuntimeConfigJson(runtimeConfig, "9999.0.0-dummy1");
+
+            // Add dummy versions in the exe
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0", "9999.0.1-dummy1");
+
+            // Version: 9999.0.0-dummy1
+            // exe: 9999.0.0, 9999.0.1-dummy1
+            // Expected: fail since we don't roll forward unless match on major.minor.patch and never roll forward to production
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Fail()
+                .And
+                .HaveStdErrContaining("It was not possible to find any compatible framework version");
+
+            // Add preview versions in the exe with name major.minor.patch
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0-dummy2", "9999.0.0-dummy3");
+
+            // Version: 9999.0.0-dummy1
+            // exe: 9999.0.0-dummy2, 9999.0.0-dummy3, 9999.0.0, 9999.0.1-dummy1
+            // Expected: 9999.0.0-dummy2
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.0.0-dummy2"));
+
+            // Verify we have the expected runtime versions
+            dotnet.Exec("--list-runtimes")
+                .WorkingDirectory(_currentWorkingDir)
+                .CaptureStdOut()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("9999.0.0-dummy2")
+                .And
+                .HaveStdOutContaining("9999.0.0-dummy3")
+                .And
+                .HaveStdOutContaining("9999.0.0")
+                .And
+                .HaveStdOutContaining("9999.0.1-dummy1");
+
+            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0-dummy2", "9999.0.0-dummy3", "9999.0.0", "9999.0.1-dummy1");
         }
 
         [Fact]
@@ -354,12 +602,10 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9998.0.1", "9998.1.0", "9999.0.0", "9999.0.1", "9999.1.0");
 
             // Version: 9999.1.1
-            // 'Roll forward on no candidate fx' enabled through env var
             // exe: 9998.0.1, 9998.1.0, 9999.0.0, 9999.0.1, 9999.1.0
             // Expected: no compatible version
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
                 .EnvironmentVariable("COREHOST_TRACE", "1")
                 .CaptureStdOut()
                 .CaptureStdErr()
@@ -372,7 +618,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             // Verify we have the expected runtime versions
             dotnet.Exec("--list-runtimes")
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
                 .CaptureStdOut()
                 .Execute()
                 .Should()
@@ -452,7 +697,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             // Verify we have the expected runtime versions
             dotnet.Exec("--list-runtimes")
                 .WorkingDirectory(_currentWorkingDir)
-                .EnvironmentVariable("COREHOST_TRACE", "1")
                 .WithUserProfile(_userDir)
                 .CaptureStdOut()
                 .Execute()
@@ -489,12 +733,14 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
 
             // Version: NetCoreApp 9999.0.0
             //          UberFramework 7777.0.0
+            // 'Roll forward on no candidate fx' disabled through env var
             // Exe: NetCoreApp 9999.1.0
             //      UberFramework 7777.0.0
             // Expected: no compatible version
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
                 .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "0")
                 .CaptureStdOut()
                 .CaptureStdErr()
                 .Execute()
@@ -503,11 +749,12 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
                 .And
                 .HaveStdErrContaining("It was not possible to find any compatible framework version");
 
-            // Enable rollForwardOnNoCandidateFx
+            // Enable rollForwardOnNoCandidateFx on app's config, which will be used as the default for Uber's config
             SetRuntimeConfigJson(runtimeConfig, "7777.0.0", rollFwdOnNoCandidateFx: 1, testConfigPropertyValue : null, useUberFramework: true);
 
             // Version: NetCoreApp 9999.0.0
             //          UberFramework 7777.0.0
+            //          'Roll forward on no candidate fx' enabled through config
             // Exe: NetCoreApp 9999.1.0
             //      UberFramework 7777.0.0
             // Expected: 9999.1.0
@@ -515,6 +762,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
                 .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "0")
                 .CaptureStdOut()
                 .CaptureStdErr()
                 .Execute()
@@ -532,6 +780,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
 
             // Version: NetCoreApp 9999.0.0
             //          UberFramework 7777.0.0
+            //          'Roll forward on no candidate fx' enabled through config
             // Exe: NetCoreApp 9999.1.0
             //      UberFramework 7777.0.0
             // Expected: 9999.1.0
@@ -539,6 +788,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
                 .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "0")
                 .CaptureStdOut()
                 .CaptureStdErr()
                 .Execute()