Skip to main content

perforce_cli/cmd/
attribute.rs

1use std::ffi::OsStr;
2use std::io::Write;
3#[cfg(not(feature = "lt2024_2"))]
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::{Child, Command, Output, Stdio};
7
8use super::{ExclusiveOption, SubCommand, Unselected};
9
10use crate::global::GlobalOpts;
11use crate::spawn::ParameterizedSpawn;
12
13/// Default value source of `p4 attribute`: set or clear attributes with
14/// `-n name [-v value]` pairs.
15///
16/// Each entry is an attribute name together with the value to set, or `None`
17/// to clear the attribute by omitting `-v`.
18#[derive(Debug, Clone, Default)]
19pub struct Standard {
20    values: Option<Vec<(String, Option<String>)>>,
21
22    hex: bool,
23}
24
25impl ExclusiveOption for Standard {
26    fn inject_args(&self, command: &mut Command) {
27        if self.hex {
28            command.arg("-e");
29        }
30
31        if let Some(pairs) = &self.values {
32            for (name, value) in pairs {
33                command.arg("-n").arg(name);
34
35                if let Some(value) = value {
36                    command.arg("-v").arg(value);
37                }
38            }
39        }
40    }
41}
42
43/// Read the attribute value from standard input (`-i`).
44///
45/// Entered with [`Attribute::read_from_stdin`]; only one file argument is
46/// allowed in this state.
47#[derive(Debug, Clone, Default)]
48pub struct FromStdin {
49    hex: bool,
50
51    name: String,
52}
53
54impl ExclusiveOption for FromStdin {
55    fn inject_args(&self, command: &mut Command) {
56        if self.hex {
57            command.arg("-e");
58        }
59
60        command.arg("-i").arg("-n").arg(&self.name);
61    }
62}
63
64/// Read the attribute value from a file (`-I filename`).
65///
66/// Entered with [`Attribute::read_from_file`]; `-e` is not available and only
67/// one file argument is allowed in this state.
68#[cfg(not(feature = "lt2024_2"))]
69#[derive(Debug, Clone, Default)]
70pub struct FromFile {
71    name: String,
72
73    file: PathBuf,
74}
75
76#[cfg(not(feature = "lt2024_2"))]
77impl ExclusiveOption for FromFile {
78    fn inject_args(&self, command: &mut Command) {
79        command.arg("-I").arg(&self.file).arg("-n").arg(&self.name);
80    }
81}
82
83/// Trait storage location of the `-T0` / `-T1` option group of
84/// `p4 attribute`.
85#[cfg(not(feature = "lt2023_2"))]
86pub mod storage {
87    /// `-T0`: store the attribute value in the `db.traits` table.
88    #[derive(Debug, Clone, Copy, Default)]
89    pub struct DatabaseTraits;
90
91    /// `-T1`: store the attribute value in the `trait` depot.
92    #[derive(Debug, Clone, Copy, Default)]
93    pub struct TraitDepot;
94}
95
96#[cfg(not(feature = "lt2023_2"))]
97impl ExclusiveOption for storage::DatabaseTraits {
98    fn inject_args(&self, command: &mut Command) {
99        command.arg("-T0");
100    }
101}
102
103#[cfg(not(feature = "lt2023_2"))]
104impl ExclusiveOption for storage::TraitDepot {
105    fn inject_args(&self, command: &mut Command) {
106        command.arg("-T1");
107    }
108}
109
110#[cfg_attr(
111    feature = "lt2023_2",
112    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p] -i -n name file`"
113)]
114#[cfg_attr(
115    all(feature = "lt2024_2", not(feature = "lt2023_2")),
116    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`"
117)]
118#[cfg_attr(
119    not(feature = "lt2024_2"),
120    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`\n\n`p4 attribute [-f -p [-T0 | -T1]] -I filename -n name file`"
121)]
122///
123/// The `S` type parameter tracks the value source: [`Standard`] for the
124/// default `-n`/`-v` set-or-clear form, [`FromStdin`] for the `-i` form,
125#[cfg_attr(
126    not(feature = "lt2024_2"),
127    doc = "[`FromFile`] for the `-I filename` form, and"
128)]
129/// and the `T` type parameter tracks the `-T0`/`-T1` trait storage location.
130#[derive(Debug, Clone, Default)]
131pub struct Attribute<S = Standard, T = Unselected> {
132    bin: PathBuf,
133
134    global_opts: GlobalOpts,
135
136    source: S,
137
138    storage: T,
139
140    on_submitted_files: bool,
141
142    propagating: bool,
143}
144
145impl<S: ExclusiveOption, T: ExclusiveOption> SubCommand for Attribute<S, T> {
146    fn name(&self) -> &str {
147        "attribute"
148    }
149
150    fn inject_local_args(&self, command: &mut Command) {
151        if self.on_submitted_files {
152            command.arg("-f");
153        }
154
155        if self.propagating {
156            command.arg("-p");
157        }
158
159        self.storage.inject_args(command);
160
161        self.source.inject_args(command);
162    }
163
164    fn global_opts(&self) -> Option<&GlobalOpts> {
165        Some(&self.global_opts)
166    }
167}
168
169impl Attribute<Standard, Unselected> {
170    /// Creates a new `p4 attribute` command.
171    ///
172    /// `bin` is the path to the Perforce command-line executable.
173    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
174        Self {
175            bin: bin.into(),
176            global_opts,
177            on_submitted_files: false,
178            propagating: false,
179            source: Standard::default(),
180            storage: Unselected,
181        }
182    }
183}
184
185impl<T: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Attribute<Standard, T>
186where
187    S: IntoIterator<Item = I>,
188    I: AsRef<OsStr>,
189{
190    type Output = Child;
191    type Error = std::io::Error;
192
193    /// Spawns `p4 attribute` for the given files as a child process with
194    /// piped standard output and error streams; use the returned [`Child`]
195    /// handle to wait for it or interact with it.
196    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
197        self.setup_command(&self.bin)
198            .args(files)
199            .stdout(Stdio::piped())
200            .stderr(Stdio::piped())
201            .spawn()
202    }
203}
204
205impl<T: ExclusiveOption> Attribute<Standard, T> {
206    /// # Description
207    ///
208    /// -n name -v value
209    ///
210    /// Sets the attribute `name` to `value` on the given files.
211    #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
212    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
213        self.source
214            .values
215            .get_or_insert_with(Vec::new)
216            .push((name.into(), Some(value.into())));
217        self
218    }
219
220    /// # Description
221    ///
222    /// -n name
223    ///
224    /// Clears the attribute `name` on the given files by omitting the `-v`
225    /// option.
226    pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
227        self.source
228            .values
229            .get_or_insert_with(Vec::new)
230            .push((name.into(), None));
231        self
232    }
233
234    /// # Description
235    ///
236    /// -e
237    ///
238    /// Indicates that the value is specified in hex.
239    pub fn get_hex(&self) -> bool {
240        self.source.hex
241    }
242
243    /// # Description
244    ///
245    /// -e
246    ///
247    /// Indicates that the value is specified in hex.
248    pub fn set_hex(&mut self, v: bool) -> &mut Self {
249        self.source.hex = v;
250        self
251    }
252
253    /// # Description
254    ///
255    /// -e
256    ///
257    /// Indicates that the value is specified in hex.
258    pub fn hex(mut self, v: bool) -> Self {
259        self.source.hex = v;
260        self
261    }
262
263    /// # Description
264    ///
265    /// -i
266    ///
267    #[cfg_attr(
268        feature = "lt2024_2",
269        doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
270    )]
271    #[cfg_attr(
272        not(feature = "lt2024_2"),
273        doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option. This option supports both textual and binary content."
274    )]
275    ///
276    /// Transitions this command to the [`FromStdin`] state; any `-n`/`-v`
277    /// pairs set with [`Self::set`] or [`Self::clear`] are discarded, while
278    /// `-e` is preserved.
279    pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
280        Attribute {
281            bin: self.bin,
282            global_opts: self.global_opts,
283            on_submitted_files: self.on_submitted_files,
284            propagating: self.propagating,
285            source: FromStdin {
286                hex: self.source.hex,
287                name,
288            },
289            storage: self.storage,
290        }
291    }
292
293    /// # Description
294    ///
295    /// -I filename
296    ///
297    /// Read the attribute value from a file. The file can have textual or
298    /// binary content. Use this when the attribute data exceeds 250
299    /// megabytes, which might cause the command to fail with the `Rpc buffer
300    /// too big` error. The following are not allowed with this option:
301    ///
302    /// - Using the `-e` option to specify the value as hex.
303    /// - More than one file argument.
304    /// - Setting more than one trait value.
305    ///
306    /// To display attributes set with this option, use the `p4 print -T`
307    /// command instead of the `p4 fstat -Oa` command because `p4 print -T`
308    /// can handle larger non-encoded binary data.
309    ///
310    /// Transitions this command to the [`FromFile`] state; any `-n`/`-v`
311    /// pairs set with [`Self::set`] or [`Self::clear`], as well as `-e`, are
312    /// discarded.
313    #[cfg(not(feature = "lt2024_2"))]
314    pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
315        Attribute {
316            bin: self.bin,
317            global_opts: self.global_opts,
318            on_submitted_files: self.on_submitted_files,
319            propagating: self.propagating,
320            source: FromFile { name, file },
321            storage: self.storage,
322        }
323    }
324}
325
326impl<T: ExclusiveOption> Attribute<FromStdin, T> {
327    /// # Description
328    ///
329    /// -e
330    ///
331    /// Indicates that the value is specified in hex.
332    pub fn get_hex(&self) -> bool {
333        self.source.hex
334    }
335
336    /// # Description
337    ///
338    /// -e
339    ///
340    /// Indicates that the value is specified in hex.
341    pub fn set_hex(&mut self, v: bool) -> &mut Self {
342        self.source.hex = v;
343        self
344    }
345
346    /// # Description
347    ///
348    /// -e
349    ///
350    /// Indicates that the value is specified in hex.
351    pub fn hex(mut self, v: bool) -> Self {
352        self.source.hex = v;
353        self
354    }
355
356    /// # Description
357    ///
358    /// -n name
359    ///
360    /// The name of the attribute read from standard input.
361    pub fn get_name(&self) -> &str {
362        &self.source.name
363    }
364
365    /// # Description
366    ///
367    /// -n name
368    ///
369    /// The name of the attribute read from standard input.
370    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
371        self.source.name = v.into();
372        self
373    }
374
375    /// # Description
376    ///
377    /// -n name
378    ///
379    /// The name of the attribute read from standard input.
380    pub fn name(mut self, v: impl Into<String>) -> Self {
381        self.source.name = v.into();
382        self
383    }
384
385    /// Runs `p4 attribute -i` for a single file, writing `value` to the
386    /// child's standard input, and captures the command's output.
387    ///
388    /// Unlike [`ParameterizedSpawn::spawn_with`], this method handles writing
389    /// the attribute value to the child's standard input itself, so it is the
390    /// most convenient way to submit values through standard input.
391    ///
392    /// Only one file argument is allowed in the [`FromStdin`] state.
393    pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
394    where
395        S: AsRef<OsStr>,
396        V: AsRef<[u8]>,
397    {
398        let mut child = self
399            .setup_command(&self.bin)
400            .arg(file)
401            .stdin(Stdio::piped())
402            .stdout(Stdio::piped())
403            .stderr(Stdio::piped())
404            .spawn()?;
405
406        if let Some(ref mut stdin) = child.stdin {
407            stdin.write_all(value.as_ref())?;
408        }
409        drop(child.stdin.take());
410
411        child.wait_with_output()
412    }
413}
414
415impl<T: ExclusiveOption, I> ParameterizedSpawn<(I, Stdio)> for Attribute<FromStdin, T>
416where
417    I: AsRef<OsStr>,
418{
419    type Output = Child;
420    type Error = std::io::Error;
421
422    /// Spawns `p4 attribute -i` for a single file as a child process with the
423    /// given standard input configuration and piped standard output and error
424    /// streams; use the returned [`Child`] handle to wait for it or interact
425    /// with it.
426    ///
427    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
428    /// write the attribute value yourself, then drop it before waiting on
429    /// the child. Only one file argument is allowed in the [`FromStdin`]
430    /// state.
431    fn spawn_with(&mut self, input: (I, Stdio)) -> Result<Self::Output, Self::Error> {
432        self.setup_command(&self.bin)
433            .arg(input.0)
434            .stdin(input.1)
435            .stdout(Stdio::piped())
436            .stderr(Stdio::piped())
437            .spawn()
438    }
439}
440
441impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromStdin, T>
442where
443    I: AsRef<OsStr>,
444{
445    type Output = Child;
446    type Error = std::io::Error;
447
448    /// Spawns `p4 attribute -i` for a single file as a child process with the
449    /// given standard input configuration and piped standard output and error
450    /// streams; use the returned [`Child`] handle to wait for it or interact
451    /// with it.
452    fn spawn_with(&mut self, (input,): (I,)) -> Result<Self::Output, Self::Error> {
453        self.setup_command(&self.bin)
454            .arg(input)
455            .stdin(Stdio::piped())
456            .stdout(Stdio::piped())
457            .stderr(Stdio::piped())
458            .spawn()
459    }
460}
461
462#[cfg(not(feature = "lt2024_2"))]
463impl<T: ExclusiveOption> Attribute<FromFile, T> {
464    /// # Description
465    ///
466    /// -n name
467    ///
468    /// The name of the attribute read from the file.
469    pub fn get_name(&self) -> &str {
470        &self.source.name
471    }
472
473    /// # Description
474    ///
475    /// -n name
476    ///
477    /// The name of the attribute read from the file.
478    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
479        self.source.name = v.into();
480        self
481    }
482
483    /// # Description
484    ///
485    /// -n name
486    ///
487    /// The name of the attribute read from the file.
488    pub fn name(mut self, v: impl Into<String>) -> Self {
489        self.source.name = v.into();
490        self
491    }
492
493    /// # Description
494    ///
495    /// -I filename
496    ///
497    /// The file the attribute value is read from.
498    pub fn get_file(&self) -> &Path {
499        &self.source.file
500    }
501}
502
503#[cfg(not(feature = "lt2024_2"))]
504impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromFile, T>
505where
506    I: AsRef<OsStr>,
507{
508    type Output = Child;
509    type Error = std::io::Error;
510
511    /// Spawns `p4 attribute -I` for a single file as a child process with
512    /// piped standard output and error streams; use the returned [`Child`]
513    /// handle to wait for it or interact with it.
514    ///
515    /// Only one file argument is allowed in the [`FromFile`] state.
516    fn spawn_with(&mut self, (file,): (I,)) -> Result<Self::Output, Self::Error> {
517        self.setup_command(&self.bin)
518            .arg(file)
519            .stdout(Stdio::piped())
520            .stderr(Stdio::piped())
521            .spawn()
522    }
523}
524
525impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
526    /// # Description
527    ///
528    /// g-opts
529    ///
530    #[cfg_attr(
531        feature = "lt2014_2",
532        doc = "See the [Global Options](GlobalOpts) section."
533    )]
534    #[cfg_attr(
535        all(feature = "lt2015_1", not(feature = "lt2014_2")),
536        doc = "See the [“Global Options”](GlobalOpts) section."
537    )]
538    #[cfg_attr(
539        all(feature = "lt2017_1", not(feature = "lt2015_1")),
540        doc = "See [“Global Options”](GlobalOpts)."
541    )]
542    #[cfg_attr(
543        all(feature = "lt2018_2", not(feature = "lt2017_1")),
544        doc = "See [Global Options](GlobalOpts)."
545    )]
546    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
547    pub fn get_global_opts(&self) -> &GlobalOpts {
548        &self.global_opts
549    }
550
551    /// # Description
552    ///
553    /// g-opts
554    ///
555    #[cfg_attr(
556        feature = "lt2014_2",
557        doc = "See the [Global Options](GlobalOpts) section."
558    )]
559    #[cfg_attr(
560        all(feature = "lt2015_1", not(feature = "lt2014_2")),
561        doc = "See the [“Global Options”](GlobalOpts) section."
562    )]
563    #[cfg_attr(
564        all(feature = "lt2017_1", not(feature = "lt2015_1")),
565        doc = "See [“Global Options”](GlobalOpts)."
566    )]
567    #[cfg_attr(
568        all(feature = "lt2018_2", not(feature = "lt2017_1")),
569        doc = "See [Global Options](GlobalOpts)."
570    )]
571    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
572    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
573        self.global_opts = v;
574        self
575    }
576
577    /// # Description
578    ///
579    /// g-opts
580    ///
581    #[cfg_attr(
582        feature = "lt2014_2",
583        doc = "See the [Global Options](GlobalOpts) section."
584    )]
585    #[cfg_attr(
586        all(feature = "lt2015_1", not(feature = "lt2014_2")),
587        doc = "See the [“Global Options”](GlobalOpts) section."
588    )]
589    #[cfg_attr(
590        all(feature = "lt2017_1", not(feature = "lt2015_1")),
591        doc = "See [“Global Options”](GlobalOpts)."
592    )]
593    #[cfg_attr(
594        all(feature = "lt2018_2", not(feature = "lt2017_1")),
595        doc = "See [Global Options](GlobalOpts)."
596    )]
597    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
598    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
599        self.global_opts = v;
600        self
601    }
602
603    /// # Description
604    ///
605    /// -f
606    ///
607    /// Set the attribute on submitted files. If a propagating trait is set
608    /// on a submitted file, a revision specifier cannot be used, and the
609    /// file must not be currently open in any workspace.
610    pub fn get_on_submitted_files(&self) -> bool {
611        self.on_submitted_files
612    }
613
614    /// # Description
615    ///
616    /// -f
617    ///
618    /// Set the attribute on submitted files. If a propagating trait is set
619    /// on a submitted file, a revision specifier cannot be used, and the
620    /// file must not be currently open in any workspace.
621    pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
622        self.on_submitted_files = v;
623        self
624    }
625
626    /// # Description
627    ///
628    /// -f
629    ///
630    /// Set the attribute on submitted files. If a propagating trait is set
631    /// on a submitted file, a revision specifier cannot be used, and the
632    /// file must not be currently open in any workspace.
633    pub fn on_submitted_files(mut self, v: bool) -> Self {
634        self.on_submitted_files = v;
635        self
636    }
637
638    /// # Description
639    ///
640    /// -p
641    ///
642    #[cfg_attr(
643        feature = "lt2021_2",
644        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
645    )]
646    #[cfg_attr(
647        not(feature = "lt2021_2"),
648        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
649    )]
650    pub fn get_propagating(&self) -> bool {
651        self.propagating
652    }
653
654    /// # Description
655    ///
656    /// -p
657    ///
658    #[cfg_attr(
659        feature = "lt2021_2",
660        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
661    )]
662    #[cfg_attr(
663        not(feature = "lt2021_2"),
664        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
665    )]
666    pub fn set_propagating(&mut self, v: bool) -> &mut Self {
667        self.propagating = v;
668        self
669    }
670
671    /// # Description
672    ///
673    /// -p
674    ///
675    #[cfg_attr(
676        feature = "lt2021_2",
677        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
678    )]
679    #[cfg_attr(
680        not(feature = "lt2021_2"),
681        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
682    )]
683    pub fn propagating(mut self, v: bool) -> Self {
684        self.propagating = v;
685        self
686    }
687}
688
689#[cfg(not(feature = "lt2023_2"))]
690impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
691    /// # Description
692    ///
693    /// -T0
694    ///
695    /// Causes the value to be stored in the `db.traits` table, which is the
696    /// implicit default.
697    pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
698        Attribute {
699            bin: self.bin,
700            global_opts: self.global_opts,
701            on_submitted_files: self.on_submitted_files,
702            propagating: self.propagating,
703            source: self.source,
704            storage: storage::DatabaseTraits,
705        }
706    }
707
708    /// # Description
709    ///
710    /// -T1
711    ///
712    /// Causes the value to be stored in the `trait` depot instead of the
713    /// `db.traits` table, even if the size of the attribute value is less
714    /// than the size specified by the `trait.storagedepot.min` configurable.
715    /// However, if `trait.storagedepot.min` is unset or set to `0`, the
716    /// attribute value is stored in the `db.traits` table.
717    pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
718        Attribute {
719            bin: self.bin,
720            global_opts: self.global_opts,
721            on_submitted_files: self.on_submitted_files,
722            propagating: self.propagating,
723            source: self.source,
724            storage: storage::TraitDepot,
725        }
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use crate::cmd::args_of;
733
734    /// Dry-run checks of the assembled `p4 attribute` command line; no
735    /// process is spawned.
736    #[test]
737    fn without_options() {
738        let attr = Attribute::new("p4", GlobalOpts::new());
739
740        assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
741    }
742
743    #[test]
744    fn set_attribute_with_value() {
745        let mut attr = Attribute::new("p4", GlobalOpts::new());
746        attr.set("status", "approved");
747
748        assert_eq!(
749            args_of(&attr.setup_command("p4")),
750            ["attribute", "-n", "status", "-v", "approved"]
751        );
752    }
753
754    #[test]
755    fn clear_attribute_omits_value() {
756        let mut attr = Attribute::new("p4", GlobalOpts::new());
757        attr.clear("status");
758
759        assert_eq!(
760            args_of(&attr.setup_command("p4")),
761            ["attribute", "-n", "status"]
762        );
763    }
764
765    #[test]
766    fn multiple_set_and_clear_pairs_keep_order() {
767        let mut attr = Attribute::new("p4", GlobalOpts::new());
768        attr.set("color", "red")
769            .clear("status")
770            .set("owner", "alice");
771
772        assert_eq!(
773            args_of(&attr.setup_command("p4")),
774            [
775                "attribute",
776                "-n",
777                "color",
778                "-v",
779                "red",
780                "-n",
781                "status",
782                "-n",
783                "owner",
784                "-v",
785                "alice"
786            ]
787        );
788    }
789
790    #[test]
791    fn hex_submitted_propagating() {
792        let mut attr = Attribute::new("p4", GlobalOpts::new());
793        attr.set_hex(true)
794            .set_on_submitted_files(true)
795            .set_propagating(true)
796            .set("thumb", "deadbeef");
797
798        assert_eq!(
799            args_of(&attr.setup_command("p4")),
800            [
801                "attribute",
802                "-f",
803                "-p",
804                "-e",
805                "-n",
806                "thumb",
807                "-v",
808                "deadbeef"
809            ]
810        );
811    }
812
813    #[test]
814    fn read_from_stdin_state() {
815        let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
816
817        assert_eq!(attr.get_name(), "thumb");
818        assert_eq!(
819            args_of(&attr.setup_command("p4")),
820            ["attribute", "-i", "-n", "thumb"]
821        );
822    }
823
824    #[test]
825    fn stdin_name_can_be_replaced() {
826        let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
827        attr.set_name("thumb");
828
829        assert_eq!(attr.get_name(), "thumb");
830
831        let attr = attr.name("icon");
832        assert_eq!(attr.get_name(), "icon");
833        assert_eq!(
834            args_of(&attr.setup_command("p4")),
835            ["attribute", "-i", "-n", "icon"]
836        );
837    }
838
839    #[test]
840    fn stdin_preserves_flags_and_hex() {
841        let mut attr = Attribute::new("p4", GlobalOpts::new());
842        attr.set_hex(true)
843            .set_on_submitted_files(true)
844            .set_propagating(true)
845            .set("discarded", "value");
846
847        let attr = attr.read_from_stdin("thumb".to_string());
848
849        assert!(attr.get_hex());
850        assert_eq!(
851            args_of(&attr.setup_command("p4")),
852            ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
853        );
854    }
855
856    #[cfg(not(feature = "lt2023_2"))]
857    #[test]
858    fn store_in_database_traits() {
859        let mut attr = Attribute::new("p4", GlobalOpts::new());
860        attr.set("thumb", "data");
861
862        let attr = attr.store_in_database_traits();
863
864        assert_eq!(
865            args_of(&attr.setup_command("p4")),
866            ["attribute", "-T0", "-n", "thumb", "-v", "data"]
867        );
868    }
869
870    #[cfg(not(feature = "lt2023_2"))]
871    #[test]
872    fn store_in_trait_depot() {
873        let mut attr = Attribute::new("p4", GlobalOpts::new());
874        attr.set("thumb", "data");
875
876        let attr = attr.store_in_trait_depot();
877
878        assert_eq!(
879            args_of(&attr.setup_command("p4")),
880            ["attribute", "-T1", "-n", "thumb", "-v", "data"]
881        );
882    }
883
884    #[cfg(not(feature = "lt2023_2"))]
885    #[test]
886    fn stdin_with_trait_depot() {
887        let attr = Attribute::new("p4", GlobalOpts::new())
888            .read_from_stdin("thumb".to_string())
889            .store_in_trait_depot();
890
891        assert_eq!(
892            args_of(&attr.setup_command("p4")),
893            ["attribute", "-T1", "-i", "-n", "thumb"]
894        );
895    }
896
897    #[cfg(not(feature = "lt2024_2"))]
898    #[test]
899    fn read_from_file_state() {
900        let attr = Attribute::new("p4", GlobalOpts::new())
901            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
902
903        assert_eq!(attr.get_name(), "thumb");
904        assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
905        assert_eq!(
906            args_of(&attr.setup_command("p4")),
907            ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
908        );
909    }
910
911    #[cfg(not(feature = "lt2024_2"))]
912    #[test]
913    fn all_modern_options() {
914        let mut attr = Attribute::new("p4", GlobalOpts::new());
915        attr.set_on_submitted_files(true).set_propagating(true);
916
917        let attr = attr
918            .store_in_trait_depot()
919            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
920
921        assert_eq!(
922            args_of(&attr.setup_command("p4")),
923            [
924                "attribute",
925                "-f",
926                "-p",
927                "-T1",
928                "-I",
929                "/tmp/data.bin",
930                "-n",
931                "thumb"
932            ]
933        );
934    }
935
936    #[test]
937    fn set_style_with_global_opts() {
938        let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
939        attr.set("status", "approved");
940
941        assert_eq!(
942            args_of(&attr.setup_command("p4")),
943            [
944                "-p",
945                "localhost:1666",
946                "attribute",
947                "-n",
948                "status",
949                "-v",
950                "approved"
951            ]
952        );
953    }
954}