kosiew commented on code in PR #24083:
URL: https://github.com/apache/datafusion/pull/24083#discussion_r3782730666


##########
xtask/src/ci_steps.rs:
##########
@@ -0,0 +1,728 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! CI steps that can be inspected or run through `cargo xtask`.
+
+use crate::Result;
+use std::env;
+use std::ffi::OsStr;
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+
+type StepResult<T> = std::result::Result<T, StepError>;
+type StepRunner = fn(&StepContext, &[String]) -> StepResult<CiCommand>;
+
+const CI_COMMAND: &str = "cargo xtask ci step";
+const CI_SHORTCUT: &str = "cargo ci-step";
+
+/// Metadata and implementation for one CI step.
+///
+/// Adding an entry to `CI_STEPS` makes the step dispatchable and includes its
+/// usage and description in the generated help text.
+struct StepInfo {
+    command: &'static str,
+    help_usage: &'static str,
+    help_examples: &'static [&'static str],
+    help_description: &'static str,
+    error_message: &'static str,
+    runner: StepRunner,
+}
+
+static CI_STEPS: &[StepInfo] = &[
+    StepInfo {
+        command: "check",
+        help_usage: "check <workspace|package> [default|no-default|feature]",
+        help_examples: &["check workspace", "check datafusion default"],
+        help_description: "Check workspace or package compilation",
+        error_message: "Cargo check step failed",
+        runner: StepContext::run_check,
+    },
+    StepInfo {
+        command: "test",
+        help_usage: "test 
<workspace|cli|doctest|ffi|benchmark-plan|benchmark-sqllogic|postgres|substrait>",
+        help_examples: &["test cli", "test workspace"],
+        help_description: "Run a CI test suite",
+        error_message: "Cargo test step failed",
+        runner: StepContext::run_test,
+    },
+];
+
+pub(crate) fn help() -> String {
+    let mut help = format!(
+        "DataFusion CI commands\n\nUsage:\n  {CI_COMMAND} <step-name> 
[args]\n\nExamples:\n",
+    );
+    for step in CI_STEPS {
+        if let Some(example) = step.help_examples.first() {
+            help.push_str(&format!("  {CI_COMMAND} {example}\n"));
+        }
+    }
+    help.push_str(&format!("  {CI_COMMAND} explain test workspace\n"));
+
+    let command_width = CI_STEPS
+        .iter()
+        .map(|step| step.command.len())
+        .max()
+        .unwrap_or_default();
+    help.push_str("\nAvailable steps:\n");
+    for step in CI_STEPS {
+        help.push_str(&format!(
+            "  {:command_width$}  {}\n",
+            step.command, step.help_description
+        ));
+    }
+
+    help.push_str(&format!(
+        "\nShortcut:\n  # `{CI_SHORTCUT}` is short for `{CI_COMMAND}`.\n  
{CI_SHORTCUT} check workspace\n\nFor more details:\n  {CI_COMMAND} check 
--help\n"
+    ));
+    help
+}
+
+fn step_help(step: &StepInfo) -> String {
+    let mut help = format!(
+        "DataFusion CI command: {}\n\n{}\n\nUsage:\n  {CI_COMMAND} 
{}\n\nExamples:\n",
+        step.command, step.help_description, step.help_usage,
+    );
+    for example in step.help_examples {
+        help.push_str(&format!("  {CI_COMMAND} {example}\n"));
+    }
+    if let Some(example) = step.help_examples.first() {
+        help.push_str(&format!(
+            "\nUse 'explain' to show the full command:\n  {CI_COMMAND} explain 
{example}\n"
+        ));
+    }
+    help
+}
+
+fn find_step(command: &str) -> Result<&'static StepInfo> {
+    CI_STEPS
+        .iter()
+        .find(|step| step.command == command)
+        .ok_or_else(|| format!("unknown CI step `{command}`"))
+}
+
+pub(crate) fn is_help_arg(arg: &str) -> bool {
+    matches!(arg, "help" | "-h" | "--help")
+}
+
+pub(crate) fn run(root: &Path, args: &[String]) -> Result<()> {
+    StepContext::new(root).run(args)
+}
+
+struct StepContext {
+    root: PathBuf,
+}
+
+impl StepContext {
+    fn new(root: &Path) -> Self {
+        Self {
+            root: root.to_path_buf(),
+        }
+    }
+
+    fn run(&self, args: &[String]) -> Result<()> {
+        match args {
+            [] => {
+                print!("{}", help());
+                Ok(())
+            }
+            [help_arg] if is_help_arg(help_arg) => {
+                print!("{}", help());
+                Ok(())
+            }
+            [step_name, help_arg] if is_help_arg(help_arg) => {
+                print!("{}", step_help(find_step(step_name)?));
+                Ok(())
+            }
+            step_args => {
+                let (action, step, command) = self.ci_step(step_args)?;
+                match action {
+                    StepAction::Execute => command.execute(step.error_message),
+                    StepAction::Explain => {
+                        command.explain();
+                        Ok(())
+                    }
+                }
+            }
+        }
+    }
+
+    /// Parses a CI step into an action and its complete command description.
+    /// Keeping execution out of this method guarantees `explain` and execution
+    /// use exactly the same program, arguments, environment, and directory.
+    fn ci_step(
+        &self,
+        args: &[String],
+    ) -> Result<(StepAction, &'static StepInfo, CiCommand)> {
+        let (action, args) = match args.split_first() {
+            Some((arg, args)) if arg == "explain" => (StepAction::Explain, 
args),
+            _ => (StepAction::Execute, args),
+        };
+        let Some((step, args)) = args.split_first() else {
+            return Err(format!("missing CI step\n\n{}", help()));
+        };
+
+        let step = find_step(step)?;
+        let command = (step.runner)(self, args).map_err(|error| 
error.render(step))?;
+        Ok((action, step, command))
+    }
+
+    fn run_check(&self, args: &[String]) -> StepResult<CiCommand> {
+        let args = args.iter().map(String::as_str).collect::<Vec<_>>();
+        let mut command = self.cargo();
+        command.args(["check", "--profile", "ci"]);
+
+        match args.as_slice() {
+            ["workspace"] => {
+                command.args([
+                    "--workspace",
+                    "--all-targets",
+                    "--features",
+                    "integration-tests",
+                    "--locked",
+                ]);
+            }
+            [package, "default"] => {
+                command.args(["--all-targets", "-p", package]);
+            }
+            [package, "no-default"] => {
+                command.args(["--no-default-features", "-p", package]);
+            }
+            [package, feature] => {
+                command.args([
+                    "--no-default-features",
+                    "-p",
+                    package,
+                    "--features",
+                    feature,
+                ]);
+            }
+            _ => return Err(StepError::Usage),
+        }
+
+        Ok(command)
+    }
+
+    fn run_test(&self, args: &[String]) -> StepResult<CiCommand> {

Review Comment:
   Could we add table-driven assertions for each `test` variant's argv, working 
directory, and explicit environment?
   
   The existing tests cover the `check` variants well, but for `test` we 
currently only inspect the formatted explanation of one variant. Since these 
commands are intended to mirror CI exactly, direct command-shape tests would 
make it easier to catch accidental changes to features, profiles, environment 
variables, or working directories in future edits.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to